Bug 1217162 - Implement Contextual Feedback on Insecure Passwords. draft
authorSean Lee <selee@mozilla.com>
Tue, 04 Oct 2016 21:25:45 +0800
changeset 427913 891e90461e128b57a8a915b901d47afc4d67231d
parent 426151 56b3f2c6f53e72698fea6c25130efceef2a26548
child 534596 ee31daa5be726e465ca90d56dbb32c01bd832a96
push id33165
push userbmo:selee@mozilla.com
push dateFri, 21 Oct 2016 04:00:09 +0000
bugs1217162
milestone52.0a1
Bug 1217162 - Implement Contextual Feedback on Insecure Passwords. 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/content/widgets/autocomplete.xml
toolkit/locales/en-US/chrome/global/autocomplete.dtd
toolkit/locales/jar.mn
toolkit/themes/linux/global/autocomplete.css
toolkit/themes/osx/global/autocomplete.css
toolkit/themes/windows/global/autocomplete.css
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1224,16 +1224,22 @@ 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
 
+#ifdef EARLY_BETA_OR_EARLIER
+pref("security.contextualFeedback.enabled", false);
+#else
+pref("security.contextualFeedback.enabled", true);
+#endif
+
 // 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
@@ -478,16 +478,34 @@ 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="insecureStyle"] {
+  -moz-binding: url("chrome://global/content/bindings/autocomplete.xml#autocomplete-richlistitem-insecure-field");
+  height: auto;
+}
+
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="insecureStyle"] > .ac-site-icon {
+  display: -moz-box;
+}
+
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="insecureStyle"] > .ac-title > .ac-text-overflow-container > .ac-title-text {
+  text-overflow: initial;
+  white-space: initial;
+}
+
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="insecureStyle"] > .ac-url {
+  /*display: -moz-box;*/
+}
+
 #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,21 +5,23 @@
 "use strict";
 
 this.EXPORTED_SYMBOLS = [ "LoginManagerContent",
                           "FormLikeFactory",
                           "UserAutoCompleteResult" ];
 
 const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
 const PASSWORD_INPUT_ADDED_COALESCING_THRESHOLD_MS = 1;
+const PREF_CONTEXTUAL_FEEDBACK_ENABLED = "security.contextualFeedback.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/Promise.jsm");
+Cu.import("resource://gre/modules/Preferences.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask", "resource://gre/modules/DeferredTask.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "LoginRecipesContent",
                                   "resource://gre/modules/LoginRecipes.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "LoginHelper",
                                   "resource://gre/modules/LoginHelper.jsm");
 
@@ -301,16 +303,17 @@ var LoginManagerContent = {
                            null;
 
     let requestData = {};
     let messageData = { formOrigin: formOrigin,
                         actionOrigin: actionOrigin,
                         searchString: aSearchString,
                         previousResult: previousResult,
                         rect: aRect,
+                        insecure: !win.isSecureContext,
                         remote: remote };
 
     return this._sendRequest(messageManager, requestData,
                              "RemoteLogins:autoCompleteLogins",
                              messageData);
   },
 
   setupProgressListener(window) {
@@ -1196,33 +1199,37 @@ 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, messageManager, insecure) {
   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 contextualFeedbackEnabled =
+    Preferences.get(PREF_CONTEXTUAL_FEEDBACK_ENABLED, false);
+
+  this._showContextualWarning = (insecure && contextualFeedbackEnabled) ? 1 : 0;
   this.searchString = aSearchString;
   this.logins = matchingLogins.sort(loginSort);
-  this.matchCount = matchingLogins.length;
+  this.matchCount = matchingLogins.length + this._showContextualWarning;
   this._messageManager = messageManager;
 
   if (this.matchCount > 0) {
     this.searchResult = Ci.nsIAutoCompleteResult.RESULT_SUCCESS;
     this.defaultIndex = 0;
   }
 }
 
@@ -1242,35 +1249,45 @@ 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.logins.length + this._showContextualWarning))
       throw new Error("Index out of range.");
 
-    return this.logins[index].username;
+    if (this._showContextualWarning && index === 0) {
+      return "This connection is not secure. Logins entered here could be compromised.";
+    }
+
+    return this.logins[index - this._showContextualWarning].username;
   },
 
   getLabelAt(index) {
     return this.getValueAt(index);
   },
 
   getCommentAt(index) {
     return "";
   },
 
   getStyleAt(index) {
+    if (index === 0 && this._showContextualWarning) {
+      return "insecureStyle";
+    }
     return "";
   },
 
   getImageAt(index) {
+    if (index === 0 && this._showContextualWarning) {
+      return "chrome://browser/skin/connection-mixed-active-loaded.svg#icon";
+    }
     return "";
   },
 
   getFinalCompleteValueAt(index) {
     return this.getValueAt(index);
   },
 
   removeValueAt(index, removeFromDB) {
--- 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, insecure, 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, null, insecure);
       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
@@ -473,19 +473,21 @@ 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 insecure = !aElement.ownerDocument.defaultView.isSecureContext;
+
     if (!this._remember) {
       setTimeout(function() {
-        aCallback.onSearchCompletion(new UserAutoCompleteResult(aSearchString, []));
+        aCallback.onSearchCompletion(new UserAutoCompleteResult(aSearchString, [], null, insecure));
       }, 0);
       return;
     }
 
     log.debug("AutoCompleteSearch invoked. Search is:", aSearchString);
 
     let previousResult;
     if (aPreviousResult) {
@@ -503,17 +505,17 @@ 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, insecure);
                                aCallback.onSearchCompletion(results);
                              })
                             .then(null, Cu.reportError);
   },
 
   stopSearch() {
     this._autoCompleteLookupPromise = null;
   },
--- a/toolkit/content/widgets/autocomplete.xml
+++ b/toolkit/content/widgets/autocomplete.xml
@@ -1,13 +1,18 @@
 <?xml version="1.0"?>
 <!-- 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/. -->
 
+ <!DOCTYPE bindings [
+ <!ENTITY % autoCompleteDTD SYSTEM "chrome://global/locale/autocomplete.dtd" >
+ %autoCompleteDTD;
+ ]>
+
 <bindings id="autocompleteBindings"
           xmlns="http://www.mozilla.org/xbl"
           xmlns:html="http://www.w3.org/1999/xhtml"
           xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
           xmlns:xbl="http://www.mozilla.org/xbl">
 
   <binding id="autocomplete" role="xul:combobox"
            extends="chrome://global/content/bindings/textbox.xml#textbox">
@@ -1445,16 +1450,75 @@ 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 align="left" class="text-link" href="https://support.mozilla.org/en-US/kb/insecure-password-warning-firefox?as=u&amp;utm_source=inproduct" value="&autocomplete.contextualFeedback.learnMore;"/>
+      </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"
+                           href="https://support.mozilla.org/en-US/kb/insecure-password-warning-firefox?as=u&amp;utm_source=inproduct"
+                           value="&autocomplete.contextualFeedback.learnMore;"
+                           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>
+  </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 +1746,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;
           }
new file mode 100644
--- /dev/null
+++ b/toolkit/locales/en-US/chrome/global/autocomplete.dtd
@@ -0,0 +1,9 @@
+<!-- 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/. -->
+
+<!-- LOCALIZATION NOTE : FILE This file contains the entities needed to -->
+<!-- LOCALIZATION NOTE : FILE use the Autocomplete. -->
+
+<!ENTITY autocomplete.contextualFeedback.description "This connection is not secure. Logins entered here could be compromised.">
+<!ENTITY autocomplete.contextualFeedback.learnMore "Learn More">
--- a/toolkit/locales/jar.mn
+++ b/toolkit/locales/jar.mn
@@ -17,16 +17,17 @@
 #endif
   locale/@AB_CD@/global/aboutServiceWorkers.dtd         (%chrome/global/aboutServiceWorkers.dtd)
   locale/@AB_CD@/global/aboutServiceWorkers.properties  (%chrome/global/aboutServiceWorkers.properties)
   locale/@AB_CD@/global/aboutSupport.dtd                (%chrome/global/aboutSupport.dtd)
   locale/@AB_CD@/global/aboutSupport.properties         (%chrome/global/aboutSupport.properties)
   locale/@AB_CD@/global/aboutTelemetry.dtd              (%chrome/global/aboutTelemetry.dtd)
   locale/@AB_CD@/global/aboutTelemetry.properties       (%chrome/global/aboutTelemetry.properties)
   locale/@AB_CD@/global/aboutWebrtc.properties          (%chrome/global/aboutWebrtc.properties)
+  locale/@AB_CD@/global/autocomplete.dtd                (%chrome/global/autocomplete.dtd)
   locale/@AB_CD@/global/autocomplete.properties         (%chrome/global/autocomplete.properties)
   locale/@AB_CD@/global/appPicker.dtd                   (%chrome/global/appPicker.dtd)
   locale/@AB_CD@/global/brand.dtd                       (generic/chrome/global/brand.dtd)
   locale/@AB_CD@/global/browser.properties              (%chrome/global/browser.properties)
   locale/@AB_CD@/global/charsetMenu.dtd                 (%chrome/global/charsetMenu.dtd)
   locale/@AB_CD@/global/charsetMenu.properties          (%chrome/global/charsetMenu.properties)
   locale/@AB_CD@/global/commonDialog.dtd                (%chrome/global/commonDialog.dtd)
   locale/@AB_CD@/global/commonDialogs.properties        (%chrome/global/commonDialogs.properties)
--- a/toolkit/themes/linux/global/autocomplete.css
+++ b/toolkit/themes/linux/global/autocomplete.css
@@ -182,8 +182,18 @@ html|span.ac-tag {
 
 toolbarpaletteitem > toolbaritem > textbox > hbox > hbox > html|*.textbox-input {
   visibility: hidden;
 }
 
 toolbarpaletteitem > toolbaritem > * > textbox > hbox > hbox > html|*.textbox-input {
   visibility: hidden;
 }
+
+/* ::::: contextual feedback ::::: */
+
+.ac-contextual-feedback-area {
+  background-color: #F6F6F6;
+}
+
+.ac-contextual-feedback-area > image {
+  list-style-image: url(chrome://browser/skin/connection-mixed-active-loaded.svg#icon);
+}
--- a/toolkit/themes/osx/global/autocomplete.css
+++ b/toolkit/themes/osx/global/autocomplete.css
@@ -167,8 +167,18 @@ html|span.ac-tag {
 
 toolbarpaletteitem > toolbaritem > textbox > hbox > hbox > html|*.textbox-input {
   visibility: hidden;
 }
 
 toolbarpaletteitem > toolbaritem > * > textbox > hbox > hbox > html|*.textbox-input {
   visibility: hidden;
 }
+
+/* ::::: contextual feedback ::::: */
+
+.ac-contextual-feedback-area {
+  background-color: #F6F6F6;
+}
+
+.ac-contextual-feedback-area > image {
+  list-style-image: url(chrome://browser/skin/connection-mixed-active-loaded.svg#icon);
+}
--- a/toolkit/themes/windows/global/autocomplete.css
+++ b/toolkit/themes/windows/global/autocomplete.css
@@ -172,8 +172,17 @@ html|span.ac-tag {
 toolbarpaletteitem > toolbaritem > textbox > hbox > hbox > html|*.textbox-input {
   visibility: hidden;
 }
 
 toolbarpaletteitem > toolbaritem > * > textbox > hbox > hbox > html|*.textbox-input {
   visibility: hidden;
 }
 
+/* ::::: contextual feedback ::::: */
+
+.ac-contextual-feedback-area {
+  background-color: #F6F6F6;
+}
+
+.ac-contextual-feedback-area > image {
+  list-style-image: url(chrome://browser/skin/connection-mixed-active-loaded.svg#icon);
+}