--- a/browser/extensions/formautofill/FormAutofillContent.jsm
+++ b/browser/extensions/formautofill/FormAutofillContent.jsm
@@ -14,17 +14,19 @@ this.EXPORTED_SYMBOLS = ["FormAutofillCo
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr, manager: Cm} = Components;
Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://formautofill/FormAutofillUtils.jsm");
-XPCOMUtils.defineLazyModuleGetter(this, "ProfileAutoCompleteResult",
+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");
const formFillController = Cc["@mozilla.org/satchel/form-fill-controller;1"]
.getService(Ci.nsIFormFillController);
@@ -114,22 +116,30 @@ AutofillProfileAutoCompleteSearch.protot
}
// Sort addresses by timeLastUsed for showing the lastest used address at top.
records.sort((a, b) => b.timeLastUsed - a.timeLastUsed);
let handler = FormAutofillContent.getFormHandler(focusedInput);
let adaptedRecords = handler.getAdaptedProfiles(records);
let allFieldNames = FormAutofillContent.getAllFieldNames(focusedInput);
- let result = new ProfileAutoCompleteResult(searchString,
- info.fieldName,
- allFieldNames,
- adaptedRecords,
- {});
-
+ let result = null;
+ if (collectionName == "addresses") {
+ result = new AddressResult(searchString,
+ info.fieldName,
+ allFieldNames,
+ adaptedRecords,
+ {});
+ } else {
+ result = new CreditCardResult(searchString,
+ info.fieldName,
+ allFieldNames,
+ adaptedRecords,
+ {});
+ }
listener.onSearchResult(this, result);
ProfileAutocomplete.setProfileAutoCompleteResult(result);
});
},
/**
* Stops an asynchronous search that is in progress
*/
--- a/browser/extensions/formautofill/ProfileAutoCompleteResult.jsm
+++ b/browser/extensions/formautofill/ProfileAutoCompleteResult.jsm
@@ -1,15 +1,15 @@
/* 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/. */
"use strict";
-this.EXPORTED_SYMBOLS = ["ProfileAutoCompleteResult"];
+this.EXPORTED_SYMBOLS = ["AddressResult", "CreditCardResult"]; /* exported AddressResult, CreditCardResult */
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://formautofill/FormAutofillUtils.jsm");
this.log = null;
FormAutofillUtils.defineLazyLogGetter(this, this.EXPORTED_SYMBOLS[0]);
@@ -42,24 +42,16 @@ class ProfileAutoCompleteResult {
} else {
this.searchResult = Ci.nsIAutoCompleteResult.RESULT_NOMATCH;
}
// An array of primary and secondary labels for each profile.
this._popupLabels = this._generateLabels(this._focusedFieldName,
this._allFieldNames,
this._matchingProfiles);
- // Add an empty result entry for footer. Its content will come from
- // the footer binding, so don't assign any value to it.
- this._popupLabels.push({
- primary: "",
- secondary: "",
- categories: FormAutofillUtils.getCategoriesFromFieldNames(allFieldNames),
- focusedCategory: FormAutofillUtils.getCategoryFromFieldName(focusedFieldName),
- });
}
/**
* @returns {number} The number of results
*/
get matchCount() {
return this._popupLabels.length;
}
@@ -74,16 +66,106 @@ class ProfileAutoCompleteResult {
* Get the secondary label based on the focused field name and related field names
* in the same form.
* @param {string} focusedFieldName The field name of the focused input
* @param {Array<Object>} allFieldNames The field names in the same section
* @param {object} profile The profile providing the labels to show.
* @returns {string} The secondary label
*/
_getSecondaryLabel(focusedFieldName, allFieldNames, profile) {
+ return "";
+ }
+
+ _generateLabels(focusedFieldName, allFieldNames, profiles) {}
+
+ /**
+ * Retrieves a result
+ * @param {number} index The index of the result requested
+ * @returns {string} The result at the specified index
+ */
+ getValueAt(index) {
+ this._checkIndexBounds(index);
+ return this._popupLabels[index].primary;
+ }
+
+ getLabelAt(index) {
+ this._checkIndexBounds(index);
+ return JSON.stringify(this._popupLabels[index]);
+ }
+
+ /**
+ * Retrieves a comment (metadata instance)
+ * @param {number} index The index of the comment requested
+ * @returns {string} The comment at the specified index
+ */
+ getCommentAt(index) {
+ this._checkIndexBounds(index);
+ return JSON.stringify(this._matchingProfiles[index]);
+ }
+
+ /**
+ * Retrieves a style hint specific to a particular index.
+ * @param {number} index The index of the style hint requested
+ * @returns {string} The style hint at the specified index
+ */
+ getStyleAt(index) {
+ this._checkIndexBounds(index);
+ if (index == this.matchCount - 1) {
+ return "autofill-footer";
+ }
+ return "autofill-profile";
+ }
+
+ /**
+ * Retrieves an image url.
+ * @param {number} index The index of the image url requested
+ * @returns {string} The image url at the specified index
+ */
+ getImageAt(index) {
+ this._checkIndexBounds(index);
+ return "";
+ }
+
+ /**
+ * Retrieves a result
+ * @param {number} index The index of the result requested
+ * @returns {string} The result at the specified index
+ */
+ getFinalCompleteValueAt(index) {
+ return this.getValueAt(index);
+ }
+
+ /**
+ * Removes a result from the resultset
+ * @param {number} index The index of the result to remove
+ * @param {boolean} removeFromDatabase TRUE for removing data from DataBase
+ * as well.
+ */
+ 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",
"given-name",
"additional-name",
"family-name",
@@ -155,75 +237,85 @@ class ProfileAutoCompleteResult {
secondary: this._getSecondaryLabel(focusedFieldName,
allFieldNames,
profile),
};
});
}
- /**
- * Retrieves a result
- * @param {number} index The index of the result requested
- * @returns {string} The result at the specified index
- */
- getValueAt(index) {
- this._checkIndexBounds(index);
- return this._popupLabels[index].primary;
- }
+}
- getLabelAt(index) {
- this._checkIndexBounds(index);
- return JSON.stringify(this._popupLabels[index]);
+class CreditCardResult extends ProfileAutoCompleteResult {
+ constructor(...args) {
+ super(...args);
+
+ // Add an empty result entry for footer.
+ this._popupLabels.push({primary: "", secondary: ""});
}
- /**
- * Retrieves a comment (metadata instance)
- * @param {number} index The index of the comment requested
- * @returns {string} The comment at the specified index
- */
- getCommentAt(index) {
- this._checkIndexBounds(index);
- return JSON.stringify(this._matchingProfiles[index]);
+ _getSecondaryLabel(focusedFieldName, allFieldNames, profile) {
+ const GROUP_FIELDS = {
+ "cc-name": [
+ "cc-name",
+ "cc-given-name",
+ "cc-additional-name",
+ "cc-family-name",
+ ],
+ "cc-exp": [
+ "cc-exp",
+ "cc-exp-month",
+ "cc-exp-year",
+ ],
+ };
+
+ const secondaryLabelOrder = [
+ "cc-number", // Credit card number
+ "cc-name", // Full name
+ "cc-exp", // Expiration date
+ ];
+
+ for (let field in GROUP_FIELDS) {
+ if (GROUP_FIELDS[field].includes(focusedFieldName)) {
+ focusedFieldName = field;
+ break;
+ }
+ }
+
+ for (const currentFieldName of secondaryLabelOrder) {
+ if (focusedFieldName == currentFieldName || !profile[currentFieldName]) {
+ continue;
+ }
+
+ let matching = GROUP_FIELDS[currentFieldName] ?
+ allFieldNames.some(fieldName => GROUP_FIELDS[currentFieldName].includes(fieldName)) :
+ allFieldNames.includes(currentFieldName);
+
+ if (matching) {
+ return profile[currentFieldName];
+ }
+ }
+
+ return ""; // Nothing matched.
}
- /**
- * Retrieves a style hint specific to a particular index.
- * @param {number} index The index of the style hint requested
- * @returns {string} The style hint at the specified index
- */
- getStyleAt(index) {
- this._checkIndexBounds(index);
- if (index == this.matchCount - 1) {
- return "autofill-footer";
- }
- return "autofill-profile";
+ _generateLabels(focusedFieldName, allFieldNames, profiles) {
+ // Skip results without a primary label.
+ return profiles.filter(profile => {
+ return !!profile[focusedFieldName];
+ }).map(profile => {
+ return {
+ primary: profile[focusedFieldName],
+ secondary: this._getSecondaryLabel(focusedFieldName,
+ allFieldNames,
+ profile),
+ };
+ });
}
- /**
- * Retrieves an image url.
- * @param {number} index The index of the image url requested
- * @returns {string} The image url at the specified index
- */
- getImageAt(index) {
+ // Always return empty string for credit card result. Since the decryption might
+ // be required of users' input, we have to to suppress AutoCompleteController
+ // from filling encrypted data directly.
+ getValueAt(index) {
this._checkIndexBounds(index);
return "";
}
-
- /**
- * Retrieves a result
- * @param {number} index The index of the result requested
- * @returns {string} The result at the specified index
- */
- getFinalCompleteValueAt(index) {
- return this.getValueAt(index);
- }
-
- /**
- * Removes a result from the resultset
- * @param {number} index The index of the result to remove
- * @param {boolean} removeFromDatabase TRUE for removing data from DataBase
- * as well.
- */
- removeValueAt(index, removeFromDatabase) {
- // There is no plan to support removing profiles via autocomplete.
- }
}
--- a/browser/extensions/formautofill/test/unit/test_profileAutocompleteResult.js
+++ b/browser/extensions/formautofill/test/unit/test_profileAutocompleteResult.js
@@ -1,10 +1,11 @@
"use strict";
+/* global AddressResult, CreditCardResult */
Cu.import("resource://formautofill/ProfileAutoCompleteResult.jsm");
let matchingProfiles = [{
guid: "test-guid-1",
"given-name": "Timothy",
"family-name": "Berners-Lee",
name: "Timothy Berners-Lee",
organization: "Sesame Street",
@@ -37,17 +38,17 @@ let allFieldNames = [
"street-address",
"address-line1",
"address-line2",
"address-line3",
"organization",
"tel",
];
-let testCases = [{
+let addressTestCases = [{
description: "Focus on an `organization` field",
options: {},
matchingProfiles,
allFieldNames,
searchString: "",
fieldName: "organization",
expected: {
searchResult: Ci.nsIAutoCompleteResult.RESULT_SUCCESS,
@@ -210,47 +211,178 @@ let testCases = [{
fieldName: "",
expected: {
searchResult: Ci.nsIAutoCompleteResult.RESULT_FAILURE,
defaultIndex: 0,
items: [],
},
}];
-add_task(async function test_all_patterns() {
- testCases.forEach(testCase => {
- do_print("Starting testcase: " + testCase.description);
- let actual = new ProfileAutoCompleteResult(testCase.searchString,
- testCase.fieldName,
- testCase.allFieldNames,
- testCase.matchingProfiles,
- testCase.options);
- let expectedValue = testCase.expected;
- let expectedItemLength = expectedValue.items.length;
- // If the last item shows up as a footer, we expect one more item
- // than expected.
- if (actual.getStyleAt(actual.matchCount - 1) == "autofill-footer") {
- expectedItemLength++;
- }
+matchingProfiles = [{
+ guid: "test-guid-1",
+ "cc-name": "Timothy Berners-Lee",
+ "cc-number": "************6785",
+ "cc-exp-month": 12,
+ "cc-exp-year": 2014,
+}, {
+ guid: "test-guid-2",
+ "cc-name": "John Doe",
+ "cc-number": "************1234",
+ "cc-exp-month": 4,
+ "cc-exp-year": 2014,
+}, {
+ guid: "test-guid-3",
+ "cc-number": "************5678",
+ "cc-exp-month": 8,
+ "cc-exp-year": 2018,
+}];
+
+allFieldNames = [
+ "cc-name",
+ "cc-number",
+ "cc-exp-month",
+ "cc-exp-year",
+];
- equal(actual.searchResult, expectedValue.searchResult);
- equal(actual.defaultIndex, expectedValue.defaultIndex);
- equal(actual.matchCount, expectedItemLength);
- expectedValue.items.forEach((item, index) => {
- equal(actual.getValueAt(index), item.value);
- equal(actual.getCommentAt(index), item.comment);
- equal(actual.getLabelAt(index), item.label);
- equal(actual.getStyleAt(index), item.style);
- equal(actual.getImageAt(index), item.image);
- });
+let creditCardTestCases = [{
+ description: "Focus on a `cc-name` field",
+ options: {},
+ matchingProfiles,
+ allFieldNames,
+ searchString: "",
+ fieldName: "cc-name",
+ expected: {
+ searchResult: Ci.nsIAutoCompleteResult.RESULT_SUCCESS,
+ defaultIndex: 0,
+ items: [{
+ value: "",
+ style: "autofill-profile",
+ comment: JSON.stringify(matchingProfiles[0]),
+ label: JSON.stringify({
+ primary: "Timothy Berners-Lee",
+ secondary: "************6785",
+ }),
+ image: "",
+ }, {
+ value: "",
+ style: "autofill-profile",
+ comment: JSON.stringify(matchingProfiles[1]),
+ label: JSON.stringify({
+ primary: "John Doe",
+ secondary: "************1234",
+ }),
+ image: "",
+ }],
+ },
+}, {
+ description: "Focus on a `cc-number` field",
+ options: {},
+ matchingProfiles,
+ allFieldNames,
+ searchString: "",
+ fieldName: "cc-number",
+ expected: {
+ searchResult: Ci.nsIAutoCompleteResult.RESULT_SUCCESS,
+ defaultIndex: 0,
+ items: [{
+ value: "",
+ style: "autofill-profile",
+ comment: JSON.stringify(matchingProfiles[0]),
+ label: JSON.stringify({
+ primary: "************6785",
+ secondary: "Timothy Berners-Lee",
+ }),
+ image: "",
+ }, {
+ value: "",
+ style: "autofill-profile",
+ comment: JSON.stringify(matchingProfiles[1]),
+ label: JSON.stringify({
+ primary: "************1234",
+ secondary: "John Doe",
+ }),
+ image: "",
+ }, {
+ value: "",
+ style: "autofill-profile",
+ comment: JSON.stringify(matchingProfiles[2]),
+ label: JSON.stringify({
+ primary: "************5678",
+ secondary: "",
+ }),
+ image: "",
+ }],
+ },
+}, {
+ description: "No matching profiles",
+ options: {},
+ matchingProfiles: [],
+ allFieldNames,
+ searchString: "",
+ fieldName: "",
+ expected: {
+ searchResult: Ci.nsIAutoCompleteResult.RESULT_NOMATCH,
+ defaultIndex: 0,
+ items: [],
+ },
+}, {
+ description: "Search with failure",
+ options: {resultCode: Ci.nsIAutoCompleteResult.RESULT_FAILURE},
+ matchingProfiles: [],
+ allFieldNames,
+ searchString: "",
+ fieldName: "",
+ expected: {
+ searchResult: Ci.nsIAutoCompleteResult.RESULT_FAILURE,
+ defaultIndex: 0,
+ items: [],
+ },
+}];
- if (expectedValue.items.length != 0) {
- Assert.throws(() => actual.getValueAt(expectedItemLength),
- /Index out of range\./);
+let testSets = [{
+ collectionConstructor: AddressResult,
+ testCases: addressTestCases,
+}, {
+ collectionConstructor: CreditCardResult,
+ testCases: creditCardTestCases,
+}];
- Assert.throws(() => actual.getLabelAt(expectedItemLength),
- /Index out of range\./);
+add_task(async function test_all_patterns() {
+ testSets.forEach(({collectionConstructor, testCases}) => {
+ testCases.forEach(testCase => {
+ do_print("Starting testcase: " + testCase.description);
+ let actual = new collectionConstructor(testCase.searchString,
+ testCase.fieldName,
+ testCase.allFieldNames,
+ testCase.matchingProfiles,
+ testCase.options);
+ let expectedValue = testCase.expected;
+ let expectedItemLength = expectedValue.items.length;
+ // If the last item shows up as a footer, we expect one more item
+ // than expected.
+ if (actual.getStyleAt(actual.matchCount - 1) == "autofill-footer") {
+ expectedItemLength++;
+ }
- Assert.throws(() => actual.getCommentAt(expectedItemLength),
- /Index out of range\./);
- }
+ equal(actual.searchResult, expectedValue.searchResult);
+ equal(actual.defaultIndex, expectedValue.defaultIndex);
+ equal(actual.matchCount, expectedItemLength);
+ expectedValue.items.forEach((item, index) => {
+ equal(actual.getValueAt(index), item.value);
+ equal(actual.getCommentAt(index), item.comment);
+ equal(actual.getLabelAt(index), item.label);
+ equal(actual.getStyleAt(index), item.style);
+ equal(actual.getImageAt(index), item.image);
+ });
+
+ if (expectedValue.items.length != 0) {
+ Assert.throws(() => actual.getValueAt(expectedItemLength),
+ /Index out of range\./);
+
+ Assert.throws(() => actual.getLabelAt(expectedItemLength),
+ /Index out of range\./);
+
+ Assert.throws(() => actual.getCommentAt(expectedItemLength),
+ /Index out of range\./);
+ }
+ });
});
});