Bug 1390433 - (From 1371149)Part 1. Show insecure field in credit card autofill dropdown instead of result when the connection is not secure. r=MattN draft
authorRay Lin <ralin@mozilla.com>
Fri, 30 Jun 2017 09:51:02 -0700
changeset 649804 fc8117e92b415e49913fc8bcba8f2cf0b87fcedb
parent 649803 4a150df7166bdd24540ac5cc7733d8a94912bbd2
child 649805 edba5dff484787fb6a2f53cfa7730ccd9bd1dd3f
push id75162
push userschung@mozilla.com
push dateMon, 21 Aug 2017 10:29:17 +0000
reviewersMattN
bugs1390433, 1371149
milestone56.0
Bug 1390433 - (From 1371149)Part 1. Show insecure field in credit card autofill dropdown instead of result when the connection is not secure. r=MattN MozReview-Commit-ID: APjaTedWUz9
browser/extensions/formautofill/FormAutofillContent.jsm
browser/extensions/formautofill/FormAutofillUtils.jsm
browser/extensions/formautofill/ProfileAutoCompleteResult.jsm
browser/extensions/formautofill/content/formautofill.css
browser/extensions/formautofill/content/formautofill.xml
browser/extensions/formautofill/locales/en-US/formautofill.properties
browser/extensions/formautofill/skin/shared/autocomplete-item.css
--- a/browser/extensions/formautofill/FormAutofillContent.jsm
+++ b/browser/extensions/formautofill/FormAutofillContent.jsm
@@ -22,16 +22,18 @@ Cu.import("resource://formautofill/FormA
 XPCOMUtils.defineLazyModuleGetter(this, "AddressResult",
                                   "resource://formautofill/ProfileAutoCompleteResult.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "CreditCardResult",
                                   "resource://formautofill/ProfileAutoCompleteResult.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FormAutofillHandler",
                                   "resource://formautofill/FormAutofillHandler.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FormLikeFactory",
                                   "resource://gre/modules/FormLikeFactory.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "InsecurePasswordUtils",
+                                  "resource://gre/modules/InsecurePasswordUtils.jsm");
 
 const formFillController = Cc["@mozilla.org/satchel/form-fill-controller;1"]
                              .getService(Ci.nsIFormFillController);
 
 // Register/unregister a constructor as a factory.
 function AutocompleteFactory() {}
 AutocompleteFactory.prototype = {
   register(targetConstructor) {
@@ -95,17 +97,17 @@ AutofillProfileAutoCompleteSearch.protot
 
     let savedFieldNames = FormAutofillContent.savedFieldNames;
 
     let focusedInput = formFillController.focusedInput;
     let info = FormAutofillContent.getInputDetails(focusedInput);
     let isAddressField = FormAutofillUtils.isAddressField(info.fieldName);
     let handler = FormAutofillContent.getFormHandler(focusedInput);
     let allFieldNames = handler.allFieldNames;
-    let filledRecordGUID = isAddressField ? handler.address.filledRecordGUID : handler.creditCards.filledRecordGUID;
+    let filledRecordGUID = isAddressField ? handler.address.filledRecordGUID : handler.creditCard.filledRecordGUID;
 
     // Fallback to form-history if ...
     //   - no profile can fill the currently-focused input.
     //   - the current form has already been populated.
     //   - (address only) less than 3 inputs are covered by all saved fields in the storage.
     if (!savedFieldNames.has(info.fieldName) || filledRecordGUID || (isAddressField &&
         allFieldNames.filter(field => savedFieldNames.has(field)).length < FormAutofillUtils.AUTOFILL_FIELDS_THRESHOLD)) {
       let formHistory = Cc["@mozilla.org/autocomplete/search;1?name=form-history"]
@@ -132,21 +134,23 @@ AutofillProfileAutoCompleteSearch.protot
       let result = null;
       if (isAddressField) {
         result = new AddressResult(searchString,
                                    info.fieldName,
                                    allFieldNames,
                                    adaptedRecords,
                                    {});
       } else {
+        let isSecure = InsecurePasswordUtils.isFormSecure(handler.form);
+
         result = new CreditCardResult(searchString,
                                       info.fieldName,
                                       allFieldNames,
                                       adaptedRecords,
-                                      {});
+                                      {isSecure});
       }
       listener.onSearchResult(this, result);
       ProfileAutocomplete.setProfileAutoCompleteResult(result);
     });
   },
 
   /**
    * Stops an asynchronous search that is in progress
--- a/browser/extensions/formautofill/FormAutofillUtils.jsm
+++ b/browser/extensions/formautofill/FormAutofillUtils.jsm
@@ -428,8 +428,12 @@ this.FormAutofillUtils = {
 };
 
 XPCOMUtils.defineLazyGetter(this.FormAutofillUtils, "DEFAULT_COUNTRY_CODE", () => {
   return Services.prefs.getCharPref("browser.search.countryCode", "US");
 });
 
 this.log = null;
 this.FormAutofillUtils.defineLazyLogGetter(this, this.EXPORTED_SYMBOLS[0]);
+
+XPCOMUtils.defineLazyGetter(FormAutofillUtils, "stringBundle", function() {
+  return Services.strings.createBundle("chrome://formautofill/locale/formautofill.properties");
+});
--- a/browser/extensions/formautofill/ProfileAutoCompleteResult.jsm
+++ b/browser/extensions/formautofill/ProfileAutoCompleteResult.jsm
@@ -3,24 +3,33 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 this.EXPORTED_SYMBOLS = ["AddressResult", "CreditCardResult"]; /* exported AddressResult, CreditCardResult */
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
+Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://formautofill/FormAutofillUtils.jsm");
 
+XPCOMUtils.defineLazyGetter(this, "gBrandBundle", function() {
+  return Services.strings.createBundle("chrome://branding/locale/brand.properties");
+});
+XPCOMUtils.defineLazyPreferenceGetter(this, "insecureWarningEnabled", "security.insecure_field_warning.contextual.enabled");
+
 this.log = null;
 FormAutofillUtils.defineLazyLogGetter(this, this.EXPORTED_SYMBOLS[0]);
 
 class ProfileAutoCompleteResult {
-  constructor(searchString, focusedFieldName, allFieldNames, matchingProfiles, {resultCode = null}) {
+  constructor(searchString, focusedFieldName, allFieldNames, matchingProfiles, {
+    resultCode = null,
+    isSecure = true,
+  }) {
     log.debug("Constructing new ProfileAutoCompleteResult:", [...arguments]);
 
     // nsISupports
     this.QueryInterface = XPCOMUtils.generateQI([Ci.nsIAutoCompleteResult]);
 
     // The user's query string
     this.searchString = searchString;
     // The field name of the focused input.
@@ -28,16 +37,18 @@ class ProfileAutoCompleteResult {
     // All field names in the form which contains the focused input.
     this._allFieldNames = allFieldNames;
     // The matching profiles contains the information for filling forms.
     this._matchingProfiles = matchingProfiles;
     // The default item that should be entered if none is selected
     this.defaultIndex = 0;
     // The reason the search failed
     this.errorDescription = "";
+    // The value used to determine whether the form is secure or not.
+    this._isSecure = isSecure;
 
     // The result code of this result object.
     if (resultCode) {
       this.searchResult = resultCode;
     } else if (matchingProfiles.length > 0) {
       this.searchResult = Ci.nsIAutoCompleteResult.RESULT_SUCCESS;
     } else {
       this.searchResult = Ci.nsIAutoCompleteResult.RESULT_NOMATCH;
@@ -142,27 +153,16 @@ class ProfileAutoCompleteResult {
   removeValueAt(index, removeFromDatabase) {
     // There is no plan to support removing profiles via autocomplete.
   }
 }
 
 class AddressResult extends ProfileAutoCompleteResult {
   constructor(...args) {
     super(...args);
-
-    // Add an empty result entry for footer. Its content will come from
-    // the footer binding, so don't assign any value to it.
-    // The additional properties: categories and focusedCategory are required of
-    // the popup to generate autofill hint on the footer.
-    this._popupLabels.push({
-      primary: "",
-      secondary: "",
-      categories: FormAutofillUtils.getCategoriesFromFieldNames(this._allFieldNames),
-      focusedCategory: FormAutofillUtils.getCategoryFromFieldName(this._focusedFieldName),
-    });
   }
 
   _getSecondaryLabel(focusedFieldName, allFieldNames, profile) {
     // We group similar fields into the same field name so we won't pick another
     // field in the same group as the secondary label.
     const GROUP_FIELDS = {
       "name": [
         "name",
@@ -228,42 +228,49 @@ class AddressResult extends ProfileAutoC
       }
     }
 
     return ""; // Nothing matched.
   }
 
   _generateLabels(focusedFieldName, allFieldNames, profiles) {
     // Skip results without a primary label.
-    return profiles.filter(profile => {
+    let labels = profiles.filter(profile => {
       return !!profile[focusedFieldName];
     }).map(profile => {
       let primaryLabel = profile[focusedFieldName];
       if (focusedFieldName == "street-address" &&
           profile["-moz-street-address-one-line"]) {
         primaryLabel = profile["-moz-street-address-one-line"];
       }
       return {
         primary: primaryLabel,
         secondary: this._getSecondaryLabel(focusedFieldName,
                                            allFieldNames,
                                            profile),
       };
     });
-  }
+    // Add an empty result entry for footer. Its content will come from
+    // the footer binding, so don't assign any value to it.
+    // The additional properties: categories and focusedCategory are required of
+    // the popup to generate autofill hint on the footer.
+    labels.push({
+      primary: "",
+      secondary: "",
+      categories: FormAutofillUtils.getCategoriesFromFieldNames(this._allFieldNames),
+      focusedCategory: FormAutofillUtils.getCategoryFromFieldName(this._focusedFieldName),
+    });
 
-
+    return labels;
+  }
 }
 
 class CreditCardResult extends ProfileAutoCompleteResult {
   constructor(...args) {
     super(...args);
-
-    // Add an empty result entry for footer.
-    this._popupLabels.push({primary: "", secondary: ""});
   }
 
   _getSecondaryLabel(focusedFieldName, allFieldNames, profile) {
     const GROUP_FIELDS = {
       "cc-name": [
         "cc-name",
         "cc-given-name",
         "cc-additional-name",
@@ -302,29 +309,65 @@ class CreditCardResult extends ProfileAu
         return profile[currentFieldName];
       }
     }
 
     return ""; // Nothing matched.
   }
 
   _generateLabels(focusedFieldName, allFieldNames, profiles) {
+    if (!this._isSecure) {
+      if (!insecureWarningEnabled) {
+        return [];
+      }
+      let brandName = gBrandBundle.GetStringFromName("brandShortName");
+
+      return [FormAutofillUtils.stringBundle.formatStringFromName("insecureFieldWarningDescription", [brandName], 1)];
+    }
+
     // Skip results without a primary label.
-    return profiles.filter(profile => {
+    let labels = profiles.filter(profile => {
       return !!profile[focusedFieldName];
     }).map(profile => {
       return {
         primary: profile[focusedFieldName],
         secondary: this._getSecondaryLabel(focusedFieldName,
                                            allFieldNames,
                                            profile),
       };
     });
+    // Add an empty result entry for footer.
+    labels.push({primary: "", secondary: ""});
+
+    return labels;
   }
 
   // Always return empty string for credit card result. Since the decryption might
-  // be required of users' input, we have to to suppress AutoCompleteController
+  // be required of users' input, we have to suppress AutoCompleteController
   // from filling encrypted data directly.
   getValueAt(index) {
     this._checkIndexBounds(index);
     return "";
   }
+
+  getLabelAt(index) {
+    this._checkIndexBounds(index);
+
+    let label = this._popupLabels[index];
+    if (typeof label == "string") {
+      return label;
+    }
+    return JSON.stringify(label);
+  }
+
+  getStyleAt(index) {
+    this._checkIndexBounds(index);
+    if (!this._isSecure && insecureWarningEnabled) {
+      return "autofill-insecureWarning";
+    }
+
+    if (index == this.matchCount - 1) {
+      return "autofill-footer";
+    }
+
+    return "autofill-profile";
+  }
 }
--- a/browser/extensions/formautofill/content/formautofill.css
+++ b/browser/extensions/formautofill/content/formautofill.css
@@ -1,31 +1,41 @@
 /* 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/. */
 
 #PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-profile"],
-#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-footer"] {
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-footer"],
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-insecureWarning"] {
   display: block;
   margin: 0;
   padding: 0;
   height: auto;
   min-height: auto;
 }
 
 #PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-profile"] {
   -moz-binding: url("chrome://formautofill/content/formautofill.xml#autocomplete-profile-listitem");
 }
 
 #PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-footer"] {
   -moz-binding: url("chrome://formautofill/content/formautofill.xml#autocomplete-profile-listitem-footer");
 }
 
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-insecureWarning"] {
+  -moz-binding: url("chrome://formautofill/content/formautofill.xml#autocomplete-creditcard-insecure-field");
+}
+
 /* Treat @collpased="true" as display: none similar to how it is for XUL elements.
  * https://developer.mozilla.org/en-US/docs/Web/CSS/visibility#Values */
 #PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-profile"][collapsed="true"],
-#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-footer"][collapsed="true"] {
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-footer"][collapsed="true"],
+#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-insecureWarning"][collapsed="true"] {
   display: none;
 }
 
 #PopupAutoComplete[firstresultstyle="autofill-profile"] {
   min-width: 150px !important;
 }
+
+#PopupAutoComplete[firstresultstyle="autofill-insecureWarning"] {
+  min-width: 200px !important;
+}
--- a/browser/extensions/formautofill/content/formautofill.xml
+++ b/browser/extensions/formautofill/content/formautofill.xml
@@ -53,16 +53,20 @@
       <method name="_onOverflow">
         <body></body>
       </method>
 
       <method name="_onUnderflow">
         <body></body>
       </method>
 
+      <method name="handleOverUnderflow">
+        <body></body>
+      </method>
+
       <method name="_adjustAutofillItemLayout">
         <body>
         <![CDATA[
           let outerBoxRect = this.parentNode.getBoundingClientRect();
 
           // Make item fit in popup as XUL box could not constrain
           // item's width
           this._itemBox.style.width = outerBoxRect.width + "px";
@@ -271,9 +275,48 @@
             this._itemBox.setAttribute("no-warning", "true");
           }
         ]]>
         </body>
       </method>
     </implementation>
   </binding>
 
+  <binding id="autocomplete-creditcard-insecure-field" extends="chrome://formautofill/content/formautofill.xml#autocomplete-profile-listitem-base">
+    <xbl:content xmlns="http://www.w3.org/1999/xhtml">
+      <div anonid="autofill-item-box" class="autofill-insecure-item">
+      </div>
+    </xbl:content>
+
+    <implementation implements="nsIDOMXULSelectControlItemElement">
+      <constructor>
+      <![CDATA[
+        this._itemBox = document.getAnonymousElementByAttribute(
+          this, "anonid", "autofill-item-box"
+        );
+
+        this._adjustAcItem();
+      ]]>
+      </constructor>
+
+      <property name="selected" onget="return this.getAttribute('selected') == 'true';">
+        <setter><![CDATA[
+          // Make this item unselectable since we see this item as a pure message.
+          return false;
+        ]]></setter>
+      </property>
+
+      <method name="_adjustAcItem">
+        <body>
+        <![CDATA[
+          this._adjustAutofillItemLayout();
+          this.setAttribute("formautofillattached", "true");
+
+          let value = this.getAttribute("ac-value");
+          this._itemBox.textContent = value;
+        ]]>
+        </body>
+      </method>
+
+    </implementation>
+  </binding>
+
 </bindings>
--- a/browser/extensions/formautofill/locales/en-US/formautofill.properties
+++ b/browser/extensions/formautofill/locales/en-US/formautofill.properties
@@ -51,8 +51,10 @@ state = State
 postalCode = Postal Code
 zip = Zip Code
 country = Country or Region
 tel = Phone
 email = Email
 cancel = Cancel
 save = Save
 countryWarningMessage = Autofill is currently available only for US addresses
+
+insecureFieldWarningDescription = %S has detected an insecure site. Credit card autofill is temporarily disabled
--- a/browser/extensions/formautofill/skin/shared/autocomplete-item.css
+++ b/browser/extensions/formautofill/skin/shared/autocomplete-item.css
@@ -9,16 +9,21 @@
 xul|richlistitem[originaltype="autofill-profile"][selected="true"] > .autofill-item-box {
   background-color: #F2F2F2;
 }
 
 xul|richlistitem[originaltype="autofill-footer"][selected="true"] > .autofill-item-box > .autofill-option-button {
   background-color: #DCDCDE;
 }
 
+xul|richlistitem[originaltype="autofill-insecureWarning"] {
+  border-bottom: 1px solid var(--panel-separator-color);
+  background-color: var(--arrowpanel-dimmed);
+}
+
 .autofill-item-box {
   --item-padding-vertical: 6px;
   --item-padding-horizontal: 10px;
   --col-spacer: 7px;
   --item-width: calc(50% - (var(--col-spacer) / 2));
   --item-text-color: -moz-FieldText;
 }
 
@@ -104,8 +109,30 @@ xul|richlistitem[originaltype="autofill-
 .autofill-footer > .autofill-option-button {
   height: 41px;
   background-color: #EDEDED;
 }
 
 .autofill-footer[no-warning="true"] > .autofill-warning {
   display: none;
 }
+
+.autofill-insecure-item {
+  box-sizing: border-box;
+  padding: 4px 0;
+  display: flex;
+  flex-direction: row;
+  flex-wrap: nowrap;
+  align-items: center;
+  color: GrayText;
+}
+
+.autofill-insecure-item::before {
+  display: block;
+  margin-inline-start: 4px;
+  margin-inline-end: 8px;
+  content: "";
+  width: 16px;
+  height: 16px;
+  background-image: url(chrome://browser/skin/connection-mixed-active-loaded.svg);
+  -moz-context-properties: fill;
+  fill: GrayText;
+}