Bug 1217162 - Implement insecure field warning on username field.; r?MattN draft seanlee/ContextualFeedback/Bug1217162
authorSean Lee <selee@mozilla.com>
Mon, 31 Oct 2016 17:57:40 +0800
branchseanlee/ContextualFeedback/Bug1217162
changeset 431701 bd15700651a92b7738f09ffac28f3028d210f2c7
parent 431500 969c3295d3aa77931cca26eddb047d9d74bd9858
child 724614 28a092e39e5abe2c2be8d11028b84a6c16eb384b
push id34085
push userbmo:selee@mozilla.com
push dateMon, 31 Oct 2016 10:29:11 +0000
reviewersMattN
bugs1217162
milestone52.0a1
Bug 1217162 - Implement insecure field warning on username field.; r?MattN MozReview-Commit-ID: 2tefQKO9eGY
browser/app/profile/firefox.js
browser/base/content/browser.css
toolkit/components/passwordmgr/LoginManagerContent.jsm
toolkit/components/passwordmgr/LoginManagerParent.jsm
toolkit/components/passwordmgr/nsLoginManager.js
toolkit/components/passwordmgr/test/mochitest/mochitest.ini
toolkit/components/passwordmgr/test/mochitest/test_basic_form_autocomplete.html
toolkit/components/passwordmgr/test/mochitest/test_insecure_form_field_autocomplete.html
toolkit/content/widgets/autocomplete.xml
toolkit/locales/en-US/chrome/passwordmgr/passwordmgr.properties
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1222,16 +1222,18 @@ pref("security.mixed_content.block_activ
 // Show degraded UI for http pages with password fields.
 // Only for Nightly, Dev Edition and early beta, not for late beta or release.
 #ifdef EARLY_BETA_OR_EARLIER
 pref("security.insecure_password.ui.enabled", true);
 #else
 pref("security.insecure_password.ui.enabled", false);
 #endif
 
+pref("security.insecure_field_warning.contextual.enabled", false);
+
 // 1 = allow MITM for certificate pinning checks.
 pref("security.cert_pinning.enforcement_level", 1);
 
 
 // Override the Gecko-default value of false for Firefox.
 pref("plain_text.wrap_long_lines", true);
 
 // If this turns true, Moz*Gesture events are not called stopPropagation()
--- a/browser/base/content/browser.css
+++ b/browser/base/content/browser.css
@@ -479,16 +479,36 @@ toolbar:not(#TabsToolbar) > #personal-bo
 #PopupAutoComplete > richlistbox > richlistitem > .ac-type-icon,
 #PopupAutoComplete > richlistbox > richlistitem > .ac-site-icon,
 #PopupAutoComplete > richlistbox > richlistitem > .ac-tags,
 #PopupAutoComplete > richlistbox > richlistitem > .ac-separator,
 #PopupAutoComplete > richlistbox > richlistitem > .ac-url {
   display: none;
 }
 
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="insecureWarning"] {
+  -moz-binding: url("chrome://global/content/bindings/autocomplete.xml#autocomplete-richlistitem-insecure-field");
+  height: auto;
+  background-color: #F6F6F6;
+}
+
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="insecureWarning"] > .ac-site-icon {
+  display: initial;
+  list-style-image: url(chrome://browser/skin/connection-mixed-active-loaded.svg#icon);
+}
+
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="insecureWarning"] > .ac-title > .ac-text-overflow-container > .ac-title-text {
+  text-overflow: initial;
+  white-space: initial;
+}
+
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="insecureWarning"] > .ac-title > label {
+  margin-inline-start: 0;
+}
+
 #PopupSearchAutoComplete {
   -moz-binding: url("chrome://browser/content/search/search.xml#browser-search-autocomplete-result-popup");
 }
 
 /* Overlay a badge on top of the icon of additional open search providers
    in the search panel. */
 .addengine-item > .button-box > .button-icon {
   -moz-binding: url("chrome://browser/content/search/search.xml#addengine-icon");
--- a/toolkit/components/passwordmgr/LoginManagerContent.jsm
+++ b/toolkit/components/passwordmgr/LoginManagerContent.jsm
@@ -5,31 +5,34 @@
 "use strict";
 
 this.EXPORTED_SYMBOLS = [ "LoginManagerContent",
                           "LoginFormFactory",
                           "UserAutoCompleteResult" ];
 
 const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
 const PASSWORD_INPUT_ADDED_COALESCING_THRESHOLD_MS = 1;
+const PREF_INSECURE_FIELD_WARNING_ENABLED = "security.insecure_field_warning.contextual.enabled";
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
 Cu.import("resource://gre/modules/InsecurePasswordUtils.jsm");
 Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask", "resource://gre/modules/DeferredTask.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FormLikeFactory",
                                   "resource://gre/modules/FormLikeFactory.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "LoginRecipesContent",
                                   "resource://gre/modules/LoginRecipes.jsm");
-
 XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
                                   "resource://gre/modules/LoginHelper.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "InsecurePasswordUtils",
+                                  "resource://gre/modules/InsecurePasswordUtils.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "gNetUtil",
                                    "@mozilla.org/network/util;1",
                                    "nsINetUtil");
 
 XPCOMUtils.defineLazyGetter(this, "log", () => {
   let logger = LoginHelper.createLogger("LoginManagerContent");
   return logger.log.bind(logger);
@@ -304,16 +307,17 @@ var LoginManagerContent = {
                            null;
 
     let requestData = {};
     let messageData = { formOrigin: formOrigin,
                         actionOrigin: actionOrigin,
                         searchString: aSearchString,
                         previousResult: previousResult,
                         rect: aRect,
+                        isSecure: InsecurePasswordUtils.isFormSecure(form),
                         remote: remote };
 
     return this._sendRequest(messageManager, requestData,
                              "RemoteLogins:autoCompleteLogins",
                              messageData);
   },
 
   setupProgressListener(window) {
@@ -1208,34 +1212,39 @@ var LoginUtils = {
     if (uriString == "")
       uriString = form.baseURI; // ala bug 297761
 
     return this._getPasswordOrigin(uriString, true);
   },
 };
 
 // nsIAutoCompleteResult implementation
-function UserAutoCompleteResult (aSearchString, matchingLogins, messageManager) {
+function UserAutoCompleteResult (aSearchString, matchingLogins, {isSecure, messageManager}) {
   function loginSort(a, b) {
     var userA = a.username.toLowerCase();
     var userB = b.username.toLowerCase();
 
     if (userA < userB)
       return -1;
 
     if (userA > userB)
       return  1;
 
     return 0;
   }
 
+  let prefShowInsecureFieldWarning =
+    Preferences.get(PREF_INSECURE_FIELD_WARNING_ENABLED, false);
+
+  this._showInsecureFieldWarning = (!isSecure && prefShowInsecureFieldWarning) ? 1 : 0;
   this.searchString = aSearchString;
   this.logins = matchingLogins.sort(loginSort);
-  this.matchCount = matchingLogins.length;
+  this.matchCount = matchingLogins.length + this._showInsecureFieldWarning;
   this._messageManager = messageManager;
+  this._stringBundle = Services.strings.createBundle("chrome://passwordmgr/locale/passwordmgr.properties");
 
   if (this.matchCount > 0) {
     this.searchResult = Ci.nsIAutoCompleteResult.RESULT_SUCCESS;
     this.defaultIndex = 0;
   }
 }
 
 UserAutoCompleteResult.prototype = {
@@ -1254,46 +1263,65 @@ UserAutoCompleteResult.prototype = {
   // Interfaces from idl...
   searchString : null,
   searchResult : Ci.nsIAutoCompleteResult.RESULT_NOMATCH,
   defaultIndex : -1,
   errorDescription : "",
   matchCount : 0,
 
   getValueAt(index) {
-    if (index < 0 || index >= this.logins.length)
+    if (index < 0 || index >= this.matchCount)
       throw new Error("Index out of range.");
 
-    return this.logins[index].username;
+    if (this._showInsecureFieldWarning && index === 0) {
+      return "";
+    }
+
+    return this.logins[index - this._showInsecureFieldWarning].username;
   },
 
   getLabelAt(index) {
-    return this.getValueAt(index);
+    if (index < 0 || index >= this.matchCount)
+      throw new Error("Index out of range.");
+
+    if (this._showInsecureFieldWarning && index === 0) {
+      return this._stringBundle.GetStringFromName("insecureFieldWarningDescription");
+    }
+
+    return this.logins[index - this._showInsecureFieldWarning].username;
   },
 
   getCommentAt(index) {
     return "";
   },
 
   getStyleAt(index) {
+    if (index == 0 && this._showInsecureFieldWarning) {
+      return "insecureWarning";
+    }
     return "";
   },
 
   getImageAt(index) {
     return "";
   },
 
   getFinalCompleteValueAt(index) {
     return this.getValueAt(index);
   },
 
   removeValueAt(index, removeFromDB) {
-    if (index < 0 || index >= this.logins.length)
+    if (index < 0 || index >= this.matchCount)
         throw new Error("Index out of range.");
 
+    if (this._showInsecureFieldWarning && index === 0) {
+      return;
+    }
+    index--;
+
     var [removedLogin] = this.logins.splice(index, 1);
 
     this.matchCount--;
     if (this.defaultIndex > this.logins.length)
       this.defaultIndex--;
 
     if (removeFromDB) {
       if (this._messageManager) {
--- a/toolkit/components/passwordmgr/LoginManagerParent.jsm
+++ b/toolkit/components/passwordmgr/LoginManagerParent.jsm
@@ -222,17 +222,17 @@ var LoginManagerParent = {
       requestId: requestId,
       logins: jsLogins,
       recipes,
     });
   }),
 
   doAutocompleteSearch: function({ formOrigin, actionOrigin,
                                    searchString, previousResult,
-                                   rect, requestId, remote }, target) {
+                                   rect, requestId, isSecure, remote }, target) {
     // Note: previousResult is a regular object, not an
     // nsIAutoCompleteResult.
 
     let searchStringLower = searchString.toLowerCase();
     let logins;
     if (previousResult &&
         searchStringLower.startsWith(previousResult.searchString.toLowerCase())) {
       log("Using previous autocomplete result");
@@ -265,17 +265,17 @@ var LoginManagerParent = {
     });
 
     // XXX In the E10S case, we're responsible for showing our own
     // autocomplete popup here because the autocomplete protocol hasn't
     // been e10s-ized yet. In the non-e10s case, our caller is responsible
     // for showing the autocomplete popup (via the regular
     // nsAutoCompleteController).
     if (remote) {
-      let results = new UserAutoCompleteResult(searchString, matchingLogins);
+      let results = new UserAutoCompleteResult(searchString, matchingLogins, {isSecure});
       AutoCompletePopup.showPopupWithResults({ browser: target.ownerDocument.defaultView, rect, results });
     }
 
     // Convert the array of nsILoginInfo to vanilla JS objects since nsILoginInfo
     // doesn't support structured cloning.
     var jsLogins = LoginHelper.loginsToVanillaObjects(matchingLogins);
     target.messageManager.sendAsyncMessage("RemoteLogins:loginsAutoCompleted", {
       requestId: requestId,
--- a/toolkit/components/passwordmgr/nsLoginManager.js
+++ b/toolkit/components/passwordmgr/nsLoginManager.js
@@ -17,16 +17,20 @@ Cu.import("resource://gre/modules/LoginM
 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
                                   "resource://gre/modules/Promise.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
                                   "resource://gre/modules/BrowserUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
                                   "resource://gre/modules/LoginHelper.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "LoginFormFactory",
+                                  "resource://gre/modules/LoginManagerContent.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "InsecurePasswordUtils",
+                                  "resource://gre/modules/InsecurePasswordUtils.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "log", () => {
   let logger = LoginHelper.createLogger("nsLoginManager");
   return logger;
 });
 
 const MS_PER_DAY = 24 * 60 * 60 * 1000;
 
@@ -473,19 +477,22 @@ LoginManager.prototype = {
    * We really ought to have a simple way for code to register an
    * auto-complete provider, and not have satchel calling pwmgr directly.
    */
   autoCompleteSearchAsync(aSearchString, aPreviousResult,
                           aElement, aCallback) {
     // aPreviousResult is an nsIAutoCompleteResult, aElement is
     // nsIDOMHTMLInputElement
 
+    let form = LoginFormFactory.createFromField(aElement);
+    let isSecure = InsecurePasswordUtils.isFormSecure(form);
+
     if (!this._remember) {
       setTimeout(function() {
-        aCallback.onSearchCompletion(new UserAutoCompleteResult(aSearchString, []));
+        aCallback.onSearchCompletion(new UserAutoCompleteResult(aSearchString, [], {isSecure}));
       }, 0);
       return;
     }
 
     log.debug("AutoCompleteSearch invoked. Search is:", aSearchString);
 
     let previousResult;
     if (aPreviousResult) {
@@ -503,17 +510,19 @@ LoginManager.prototype = {
                                // 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 UserAutoCompleteResult(aSearchString, logins, messageManager);
+                                 new UserAutoCompleteResult(aSearchString, logins, {
+                                   messageManager, isSecure
+                                 });
                                aCallback.onSearchCompletion(results);
                              })
                             .then(null, Cu.reportError);
   },
 
   stopSearch() {
     this._autoCompleteLookupPromise = null;
   },
--- a/toolkit/components/passwordmgr/test/mochitest/mochitest.ini
+++ b/toolkit/components/passwordmgr/test/mochitest/mochitest.ini
@@ -21,16 +21,18 @@ skip-if = toolkit == 'android' # Bug 125
 [test_basic_form_0pw.html]
 [test_basic_form_1pw.html]
 [test_basic_form_1pw_2.html]
 [test_basic_form_2pw_1.html]
 [test_basic_form_2pw_2.html]
 [test_basic_form_3pw_1.html]
 [test_basic_form_autocomplete.html]
 skip-if = toolkit == 'android' # android:autocomplete.
+[test_insecure_form_field_autocomplete.html]
+skip-if = toolkit == 'android' # android:autocomplete.
 [test_basic_form_html5.html]
 [test_basic_form_pwevent.html]
 [test_basic_form_pwonly.html]
 [test_bug_627616.html]
 skip-if = toolkit == 'android' # Tests desktop prompts
 [test_bug_776171.html]
 [test_case_differences.html]
 skip-if = toolkit == 'android' # autocomplete
--- a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_autocomplete.html
+++ b/toolkit/components/passwordmgr/test/mochitest/test_basic_form_autocomplete.html
@@ -204,16 +204,17 @@ function sendFakeAutocompleteEvent(eleme
   element.dispatchEvent(acEvent);
 }
 
 function spinEventLoop() {
   return Promise.resolve();
 }
 
 add_task(function* setup() {
+  yield SpecialPowers.pushPrefEnv({"set": [["security.insecure_field_warning.contextual.enabled", false]]});
   listenForUnexpectedPopupShown();
 });
 
 add_task(function* test_form1_initial_empty() {
   yield SimpleTest.promiseFocus(window);
 
   // Make sure initial form is empty.
   checkACForm("", "");
copy from toolkit/components/passwordmgr/test/mochitest/test_basic_form_autocomplete.html
copy to toolkit/components/passwordmgr/test/mochitest/test_insecure_form_field_autocomplete.html
--- a/toolkit/components/passwordmgr/test/mochitest/test_basic_form_autocomplete.html
+++ b/toolkit/components/passwordmgr/test/mochitest/test_insecure_form_field_autocomplete.html
@@ -204,82 +204,105 @@ function sendFakeAutocompleteEvent(eleme
   element.dispatchEvent(acEvent);
 }
 
 function spinEventLoop() {
   return Promise.resolve();
 }
 
 add_task(function* setup() {
+  yield SpecialPowers.pushPrefEnv({"set": [["security.insecure_field_warning.contextual.enabled", true]]});
   listenForUnexpectedPopupShown();
 });
 
 add_task(function* test_form1_initial_empty() {
   yield SimpleTest.promiseFocus(window);
 
   // Make sure initial form is empty.
   checkACForm("", "");
   let popupState = yield getPopupState();
   is(popupState.open, false, "Check popup is initially closed");
 });
 
+add_task(function* test_form1_warning_entry() {
+  yield SimpleTest.promiseFocus(window);
+  // Trigger autocomplete popup
+  restoreForm();
+  let shownPromise = promiseACShown();
+  doKey("down"); // open
+  yield shownPromise;
+
+  let popupState = yield getPopupState();
+  is(popupState.selectedIndex, -1, "Check no entries are selected upon opening");
+
+  doKey("down"); // select insecure warning
+  checkACForm("", ""); // value shouldn't update just by selecting
+  doKey("return"); // not "enter"!
+  yield spinEventLoop(); // let focus happen
+  checkACForm("", "");
+});
+
 add_task(function* test_form1_first_entry() {
   yield SimpleTest.promiseFocus(window);
   // Trigger autocomplete popup
   restoreForm();
   let shownPromise = promiseACShown();
   doKey("down"); // open
   yield shownPromise;
 
   let popupState = yield getPopupState();
   is(popupState.selectedIndex, -1, "Check no entries are selected upon opening");
 
+  doKey("down"); // skip insecure warning
   doKey("down"); // first
   checkACForm("", ""); // value shouldn't update just by selecting
   doKey("return"); // not "enter"!
   yield promiseFormsProcessed();
   checkACForm("tempuser1", "temppass1");
 });
 
 add_task(function* test_form1_second_entry() {
   // Trigger autocomplete popup
   restoreForm();
   let shownPromise = promiseACShown();
   doKey("down"); // open
   yield shownPromise;
 
+  doKey("down"); // skip insecure warning
   doKey("down"); // first
   doKey("down"); // second
   doKey("return"); // not "enter"!
   yield promiseFormsProcessed();
   checkACForm("testuser2", "testpass2");
 });
 
 add_task(function* test_form1_third_entry() {
   // Trigger autocomplete popup
   restoreForm();
   let shownPromise = promiseACShown();
   doKey("down"); // open
   yield shownPromise;
 
+  doKey("down"); // skip insecure warning
   doKey("down"); // first
   doKey("down"); // second
   doKey("down"); // third
   doKey("return");
   yield promiseFormsProcessed();
   checkACForm("testuser3", "testpass3");
 });
 
 add_task(function* test_form1_fourth_entry() {
   // Trigger autocomplete popup
   restoreForm();
   let shownPromise = promiseACShown();
   doKey("down"); // open
   yield shownPromise;
 
+  doKey("down"); // skip insecure warning
   doKey("down"); // first
   doKey("down"); // second
   doKey("down"); // third
   doKey("down"); // fourth
   doKey("return");
   yield promiseFormsProcessed();
   checkACForm("zzzuser4", "zzzpass4");
 });
@@ -287,21 +310,23 @@ add_task(function* test_form1_fourth_ent
 add_task(function* test_form1_wraparound_first_entry() {
   // Trigger autocomplete popup
   restoreForm();
   yield spinEventLoop(); // let focus happen
   let shownPromise = promiseACShown();
   doKey("down"); // open
   yield shownPromise;
 
+  doKey("down"); // skip insecure warning
   doKey("down"); // first
   doKey("down"); // second
   doKey("down"); // third
   doKey("down"); // fourth
   doKey("down"); // deselects
+  doKey("down"); // skip insecure warning
   doKey("down"); // first
   doKey("return");
   yield promiseFormsProcessed();
   checkACForm("tempuser1", "temppass1");
 });
 
 add_task(function* test_form1_wraparound_up_last_entry() {
   // Trigger autocomplete popup
@@ -337,71 +362,75 @@ add_task(function* test_form1_wraparound
   doKey("down"); // open
   yield shownPromise;
 
   doKey("down");
   doKey("up"); // deselects
   doKey("up"); // last entry
   doKey("up");
   doKey("up");
+  doKey("up"); // skip insecure warning
   doKey("up"); // first entry
   doKey("up"); // deselects
   doKey("up"); // last entry
   doKey("return");
   yield promiseFormsProcessed();
   checkACForm("zzzuser4", "zzzpass4");
 });
 
 add_task(function* test_form1_fill_username_without_autofill_right() {
   restoreForm();
   let shownPromise = promiseACShown();
   doKey("down"); // open
   yield shownPromise;
 
   // Set first entry w/o triggering autocomplete
+  doKey("down"); // skip insecure warning
   doKey("down"); // first
   doKey("right");
   yield spinEventLoop();
   checkACForm("tempuser1", ""); // empty password
 });
 
 add_task(function* test_form1_fill_username_without_autofill_left() {
   restoreForm();
   let shownPromise = promiseACShown();
   doKey("down"); // open
   yield shownPromise;
 
   // Set first entry w/o triggering autocomplete
+  doKey("down"); // skip insecure warning
   doKey("down"); // first
   doKey("left");
   checkACForm("tempuser1", ""); // empty password
 });
 
 add_task(function* test_form1_pageup_first() {
   restoreForm();
   let shownPromise = promiseACShown();
   doKey("down"); // open
   yield shownPromise;
 
   // Check first entry (page up)
   doKey("down"); // first
   doKey("down"); // second
   doKey("page_up"); // first
+  doKey("down"); // skip insecure warning
   doKey("return");
   yield promiseFormsProcessed();
   checkACForm("tempuser1", "temppass1");
 });
 
 add_task(function* test_form1_pagedown_last() {
   restoreForm();
   let shownPromise = promiseACShown();
   doKey("down"); // open
   yield shownPromise;
 
-  /* test 13 */
+  // test 13
   // Check last entry (page down)
   doKey("down"); // first
   doKey("page_down"); // last
   doKey("return");
   yield promiseFormsProcessed();
   checkACForm("zzzuser4", "zzzpass4");
 });
 
@@ -423,23 +452,24 @@ add_task(function* test_form1_delete() {
   doKey("down"); // open
   yield shownPromise;
 
   // XXX tried sending character "t" before/during dropdown to test
   // filtering, but had no luck. Seemed like the character was getting lost.
   // Setting uname.value didn't seem to work either. This works with a human
   // driver, so I'm not sure what's up.
 
+  doKey("down"); // skip insecure warning
   // Delete the first entry (of 4), "tempuser1"
   doKey("down");
   var numLogins;
   numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
   is(numLogins, 5, "Correct number of logins before deleting one");
 
-  let countChangedPromise = notifyMenuChanged(3);
+  let countChangedPromise = notifyMenuChanged(4);
   var deletionPromise = promiseStorageChanged(["removeLogin"]);
   // On OS X, shift-backspace and shift-delete work, just delete does not.
   // On Win/Linux, shift-backspace does not work, delete and shift-delete do.
   doKey("delete", shiftModifier);
   yield deletionPromise;
 
   checkACForm("", "");
   numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
@@ -451,29 +481,31 @@ add_task(function* test_form1_delete() {
 });
 
 add_task(function* test_form1_first_after_deletion() {
   restoreForm();
   let shownPromise = promiseACShown();
   doKey("down"); // open
   yield shownPromise;
 
+  doKey("down"); // skip insecure warning
   // Check the new first entry (of 3)
   doKey("down");
   doKey("return");
   yield promiseFormsProcessed();
   checkACForm("testuser2", "testpass2");
 });
 
 add_task(function* test_form1_delete_second() {
   restoreForm();
   let shownPromise = promiseACShown();
   doKey("down"); // open
   yield shownPromise;
 
+  doKey("down"); // skip insecure warning
   // Delete the second entry (of 3), "testuser3"
   doKey("down");
   doKey("down");
   doKey("delete", shiftModifier);
   checkACForm("", "");
   numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
   is(numLogins, 3, "Correct number of logins after deleting one");
   doKey("return");
@@ -482,30 +514,32 @@ add_task(function* test_form1_delete_sec
 });
 
 add_task(function* test_form1_first_after_deletion2() {
   restoreForm();
   let shownPromise = promiseACShown();
   doKey("down"); // open
   yield shownPromise;
 
+  doKey("down"); // skip insecure warning
   // Check the new first entry (of 2)
   doKey("down");
   doKey("return");
   yield promiseFormsProcessed();
   checkACForm("testuser2", "testpass2");
 });
 
 add_task(function* test_form1_delete_last() {
   restoreForm();
   let shownPromise = promiseACShown();
   doKey("down"); // open
   yield shownPromise;
 
-  /* test 54 */
+  doKey("down"); // skip insecure warning
+  // test 54
   // Delete the last entry (of 2), "zzzuser4"
   doKey("down");
   doKey("down");
   doKey("delete", shiftModifier);
   checkACForm("", "");
   numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
   is(numLogins, 2, "Correct number of logins after deleting one");
   doKey("return");
@@ -514,54 +548,57 @@ add_task(function* test_form1_delete_las
 });
 
 add_task(function* test_form1_first_after_3_deletions() {
   restoreForm();
   let shownPromise = promiseACShown();
   doKey("down"); // open
   yield shownPromise;
 
+  doKey("down"); // skip insecure warning
   // Check the only remaining entry
   doKey("down");
   doKey("return");
   yield promiseFormsProcessed();
   checkACForm("testuser2", "testpass2");
 });
 
 add_task(function* test_form1_check_only_entry_remaining() {
   restoreForm();
   let shownPromise = promiseACShown();
   doKey("down"); // open
   yield shownPromise;
 
-  /* test 56 */
+  doKey("down"); // skip insecure warning
+  // test 56
   // Delete the only remaining entry, "testuser2"
   doKey("down");
   doKey("delete", shiftModifier);
   //doKey("return");
   checkACForm("", "");
   numLogins = LoginManager.countLogins("http://mochi.test:8888", "http://autocomplete:8888", null);
   is(numLogins, 1, "Correct number of logins after deleting one");
 
   // remove the login that's not shown in the list.
   setupScript.sendSyncMessage("removeLogin", "login0");
 });
 
-/* Tests for single-user forms for ignoring autocomplete=off */
+// Tests for single-user forms for ignoring autocomplete=off
 add_task(function* test_form2() {
   // Turn our attention to form2
   uname = $_(2, "uname");
   pword = $_(2, "pword");
   checkACForm("singleuser5", "singlepass5");
 
   restoreForm();
   let shownPromise = promiseACShown();
   doKey("down"); // open
   yield shownPromise;
 
+  doKey("down"); // skip insecure warning
   // Check first entry
   doKey("down");
   checkACForm("", ""); // value shouldn't update
   doKey("return"); // not "enter"!
   yield promiseFormsProcessed();
   checkACForm("singleuser5", "singlepass5");
 });
 
@@ -569,16 +606,17 @@ add_task(function* test_form3() {
   uname = $_(3, "uname");
   pword = $_(3, "pword");
   checkACForm("singleuser5", "singlepass5");
   restoreForm();
   let shownPromise = promiseACShown();
   doKey("down"); // open
   yield shownPromise;
 
+  doKey("down"); // skip insecure warning
   // Check first entry
   doKey("down");
   checkACForm("", ""); // value shouldn't update
   doKey("return"); // not "enter"!
   yield promiseFormsProcessed();
   checkACForm("singleuser5", "singlepass5");
 });
 
@@ -586,16 +624,17 @@ add_task(function* test_form4() {
   uname = $_(4, "uname");
   pword = $_(4, "pword");
   checkACForm("singleuser5", "singlepass5");
   restoreForm();
   let shownPromise = promiseACShown();
   doKey("down"); // open
   yield shownPromise;
 
+  doKey("down"); // skip insecure warning
   // Check first entry
   doKey("down");
   checkACForm("", ""); // value shouldn't update
   doKey("return"); // not "enter"!
   yield promiseFormsProcessed();
   checkACForm("singleuser5", "singlepass5");
 });
 
@@ -603,16 +642,17 @@ add_task(function* test_form5() {
   uname = $_(5, "uname");
   pword = $_(5, "pword");
   checkACForm("singleuser5", "singlepass5");
   restoreForm();
   let shownPromise = promiseACShown();
   doKey("down"); // open
   yield shownPromise;
 
+  doKey("down"); // skip insecure warning
   // Check first entry
   doKey("down");
   checkACForm("", ""); // value shouldn't update
   doKey("return"); // not "enter"!
   yield promiseFormsProcessed();
   checkACForm("singleuser5", "singlepass5");
 });
 
@@ -658,16 +698,17 @@ add_task(function* test_form7() {
 });
 
 add_task(function* test_form7_2() {
   restoreForm();
   let shownPromise = promiseACShown();
   doKey("down"); // open
   yield shownPromise;
 
+  doKey("down"); // skip insecure warning
   // Check first entry
   doKey("down");
   checkACForm("", ""); // value shouldn't update
   doKey("return"); // not "enter"!
   // The form changes, so we expect the old username field to get the
   // selected autocomplete value, but neither the new username field nor
   // the password field should have any values filled in.
   yield spinEventLoop();
@@ -713,41 +754,44 @@ add_task(function* test_form9_filtering(
   checkACForm("form9userAB", "");
   uname.focus();
   doKey("left");
   shownPromise = promiseACShown();
   sendChar("A");
   let results = yield shownPromise;
 
   checkACForm("form9userAAB", "");
-  checkArrayValues(results, ["form9userAAB"], "Check dropdown is updated after inserting 'A'");
+  checkArrayValues(results, ["This connection is not secure. Logins entered here could be compromised.","form9userAAB"], "Check dropdown is updated after inserting 'A'");
+  doKey("down"); // skip insecure warning
   doKey("down");
   doKey("return");
   yield promiseFormsProcessed();
   checkACForm("form9userAAB", "form9pass");
 });
 
 add_task(function* test_form9_autocomplete_cache() {
   // Note that this addLogin call will only be seen by the autocomplete
   // attempt for the sendChar if we do not successfully cache the
   // autocomplete results.
   setupScript.sendSyncMessage("addLogin", "login8C");
   uname.focus();
-  let promise0 = notifyMenuChanged(0);
+  let promise0 = notifyMenuChanged(1);
+  let shownPromise = promiseACShown();
   sendChar("z");
   yield promise0;
+  yield shownPromise;
   let popupState = yield getPopupState();
-  is(popupState.open, false, "Check popup shouldn't open");
+  is(popupState.open, true, "Check popup should open");
 
   // check that empty results are cached - bug 496466
-  promise0 = notifyMenuChanged(0);
+  promise0 = notifyMenuChanged(1);
   sendChar("z");
   yield promise0;
   popupState = yield getPopupState();
-  is(popupState.open, false, "Check popup stays closed due to cached empty result");
+  is(popupState.open, true, "Check popup stays opened due to cached empty result");
 });
 
 add_task(function* test_form11_recipes() {
   yield loadRecipes({
     siteRecipes: [{
       "hosts": ["mochi.test:8888"],
       "usernameSelector": "input[name='1']",
       "passwordSelector": "input[name='2']"
@@ -762,16 +806,17 @@ add_task(function* test_form11_recipes()
   pword.type = "password";
   yield promiseFormsProcessed();
   restoreForm();
   checkACForm("", "");
   let shownPromise = promiseACShown();
   doKey("down"); // open
   yield shownPromise;
 
+  doKey("down"); // skip insecure warning
   doKey("down");
   checkACForm("", ""); // value shouldn't update
   doKey("return"); // not "enter"!
   yield promiseFormsProcessed();
   checkACForm("testuser10", "testpass10");
 
   // Now test recipes with blur on the username field.
   restoreForm();
@@ -789,16 +834,17 @@ add_task(function* test_form12_formless(
   uname = $_(12, "uname");
   pword = $_(12, "pword");
   restoreForm();
   checkACForm("", "");
   let shownPromise = promiseACShown();
   doKey("down"); // open
   yield shownPromise;
 
+  doKey("down"); // skip insecure warning
   // Trigger autocomplete
   doKey("down");
   checkACForm("", ""); // value shouldn't update
   let processedPromise = promiseFormsProcessed();
   doKey("return"); // not "enter"!
   yield processedPromise;
   checkACForm("testuser", "testpass");
 });
--- a/toolkit/content/widgets/autocomplete.xml
+++ b/toolkit/content/widgets/autocomplete.xml
@@ -1445,16 +1445,89 @@ extends="chrome://global/content/binding
             delete this._adjustHeightOnPopupShown;
             this.adjustHeight();
           }
       ]]>
       </handler>
     </handlers>
   </binding>
 
+  <binding id="autocomplete-richlistitem-insecure-field" extends="chrome://global/content/bindings/autocomplete.xml#autocomplete-richlistitem">
+    <content align="center"
+             onoverflow="this._onOverflow();"
+             onunderflow="this._onUnderflow();">
+      <xul:image anonid="type-icon"
+                 class="ac-type-icon"
+                 xbl:inherits="selected,current,type"/>
+      <xul:image anonid="site-icon"
+                 class="ac-site-icon"
+                 xbl:inherits="src=image,selected,type"/>
+      <xul:vbox class="ac-title"
+                align="left"
+                xbl:inherits="">
+        <xul:description class="ac-text-overflow-container">
+          <xul:description anonid="title-text"
+                           class="ac-title-text"
+                           xbl:inherits="selected"/>
+        </xul:description>
+        <xul:label id="learnMoreLink" align="left" class="text-link"/>
+      </xul:vbox>
+      <xul:hbox anonid="tags"
+                class="ac-tags"
+                align="center"
+                xbl:inherits="selected">
+        <xul:description class="ac-text-overflow-container">
+          <xul:description anonid="tags-text"
+                           class="ac-tags-text"
+                           xbl:inherits="selected"/>
+        </xul:description>
+      </xul:hbox>
+      <xul:hbox anonid="separator"
+                class="ac-separator"
+                align="center"
+                xbl:inherits="selected,actiontype,type">
+        <xul:description class="ac-separator-text">—</xul:description>
+      </xul:hbox>
+      <xul:hbox class="ac-url"
+                align="center"
+                xbl:inherits="selected,actiontype">
+        <xul:description class="ac-text-overflow-container">
+          <xul:description anonid="url-text"
+                           class="ac-url-text"
+                           xbl:inherits="selected"/>
+        </xul:description>
+      </xul:hbox>
+      <xul:hbox class="ac-action"
+                align="center"
+                xbl:inherits="selected,actiontype">
+        <xul:description class="ac-text-overflow-container">
+          <xul:description anonid="action-text"
+                           class="ac-action-text"
+                           xbl:inherits="selected"/>
+        </xul:description>
+      </xul:hbox>
+    </content>
+    <implementation>
+      <constructor><![CDATA[
+        let learnMoreLink = document.getElementById("learnMoreLink");
+        learnMoreLink.setAttribute("value", this._stringBundle.GetStringFromName("insecureFieldWarningLearnMore"));
+        learnMoreLink.setAttribute("href", "https://support.mozilla.org/kb/insecure-password-warning-firefox?as=u&utm_source=inproduct");
+      ]]></constructor>
+
+      <property name="_stringBundle">
+        <getter><![CDATA[
+          if (!this.__stringBundle) {
+            this.__stringBundle = Services.strings.createBundle("chrome://passwordmgr/locale/passwordmgr.properties");
+          }
+          return this.__stringBundle;
+        ]]></getter>
+      </property>
+    </implementation>
+  </binding>
+
   <binding id="autocomplete-richlistitem" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
 
     <content align="center"
              onoverflow="this._onOverflow();"
              onunderflow="this._onUnderflow();">
       <xul:image anonid="type-icon"
                  class="ac-type-icon"
                  xbl:inherits="selected,current,type"/>
@@ -1682,16 +1755,19 @@ extends="chrome://global/content/binding
 
       <method name="_setUpDescription">
         <parameter name="aDescriptionElement"/>
         <parameter name="aText"/>
         <parameter name="aNoEmphasis"/>
         <body>
           <![CDATA[
           // Get rid of all previous text
+          if (!aDescriptionElement) {
+            return;
+          }
           while (aDescriptionElement.hasChildNodes())
             aDescriptionElement.removeChild(aDescriptionElement.firstChild);
 
           // If aNoEmphasis is specified, don't add any emphasis
           if (aNoEmphasis) {
             aDescriptionElement.appendChild(document.createTextNode(aText));
             return;
           }
--- a/toolkit/locales/en-US/chrome/passwordmgr/passwordmgr.properties
+++ b/toolkit/locales/en-US/chrome/passwordmgr/passwordmgr.properties
@@ -60,8 +60,11 @@ loginsDescriptionFiltered=The following 
 # 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.
+
+insecureFieldWarningDescription = This connection is not secure. Logins entered here could be compromised.
+insecureFieldWarningLearnMore = Learn More