--- a/browser/extensions/formautofill/FormAutofillHandler.jsm
+++ b/browser/extensions/formautofill/FormAutofillHandler.jsm
@@ -123,30 +123,29 @@ FormAutofillHandler.prototype = {
let value = profile[fieldDetail.fieldName];
if (element instanceof Ci.nsIDOMHTMLInputElement && !element.value && value) {
if (element !== focusedInput) {
element.setUserInput(value);
}
this.changeFieldState(fieldDetail, "AUTO_FILLED");
} else if (element instanceof Ci.nsIDOMHTMLSelectElement) {
- for (let option of element.options) {
- if (value === option.textContent || value === option.value) {
- // Do not change value if the option is already selected.
- // Use case for multiple select is not considered here.
- if (option.selected) {
- break;
- }
- option.selected = true;
- element.dispatchEvent(new element.ownerGlobal.UIEvent("input", {bubbles: true}));
- element.dispatchEvent(new element.ownerGlobal.Event("change", {bubbles: true}));
- this.changeFieldState(fieldDetail, "AUTO_FILLED");
- break;
- }
+ let option = FormAutofillUtils.findSelectOption(element, profile, fieldDetail.fieldName);
+ if (!option) {
+ continue;
}
+ // Do not change value or dispatch events if the option is already selected.
+ // Use case for multiple select is not considered here.
+ if (!option.selected) {
+ option.selected = true;
+ element.dispatchEvent(new element.ownerGlobal.UIEvent("input", {bubbles: true}));
+ element.dispatchEvent(new element.ownerGlobal.Event("change", {bubbles: true}));
+ }
+ // Autofill highlight appears regardless if value is changed or not
+ this.changeFieldState(fieldDetail, "AUTO_FILLED");
}
// Unlike using setUserInput directly, FormFillController dispatches an
// asynchronous "DOMAutoComplete" event with an "input" event follows right
// after. So, we need to suppress the first "input" event fired off from
// focused input to make sure the latter change handler won't be affected
// by auto filling.
if (element === focusedInput) {
@@ -205,23 +204,35 @@ FormAutofillHandler.prototype = {
*/
previewFormFields(profile) {
log.debug("preview profile in autofillFormFields:", profile);
for (let fieldDetail of this.fieldDetails) {
let element = fieldDetail.elementWeakRef.get();
let value = profile[fieldDetail.fieldName] || "";
- // Skip the field that is null or already has text entered
- if (!element || element.value) {
+ // Skip the field that is null
+ if (!element) {
continue;
}
- element.previewValue = value;
- this.changeFieldState(fieldDetail, value ? "PREVIEW" : "NORMAL");
+ if (element instanceof Ci.nsIDOMHTMLSelectElement) {
+ // Unlike text input, select element is always previewed even if
+ // the option is already selected.
+ let option = FormAutofillUtils.findSelectOption(element, profile, fieldDetail.fieldName);
+ element.previewValue = option ? option.text : "";
+ this.changeFieldState(fieldDetail, option ? "PREVIEW" : "NORMAL");
+ } else {
+ // Skip the field if it already has text entered
+ if (element.value) {
+ continue;
+ }
+ element.previewValue = value;
+ this.changeFieldState(fieldDetail, value ? "PREVIEW" : "NORMAL");
+ }
}
},
/**
* Clear preview text and background highlight of all fields.
*/
clearPreviewedFormFields() {
log.debug("clear previewed fields in:", this.form);
--- a/browser/extensions/formautofill/FormAutofillNameUtils.jsm
+++ b/browser/extensions/formautofill/FormAutofillNameUtils.jsm
@@ -8,16 +8,18 @@ const {classes: Cc, interfaces: Ci, util
// Cu.import loads jsm files based on ISO-Latin-1 for now (see bug 530257).
// However, the references about name parts include multi-byte characters.
// Thus, we use |loadSubScript| to load the references instead.
const NAME_REFERENCES = "chrome://formautofill/content/nameReferences.js";
this.EXPORTED_SYMBOLS = ["FormAutofillNameUtils"];
+Cu.import("resource://formautofill/FormAutofillUtils.jsm");
+
// FormAutofillNameUtils is initially translated from
// https://cs.chromium.org/chromium/src/components/autofill/core/browser/autofill_data_util.cc?rcl=b861deff77abecff11ae6a9f6946e9cc844b9817
var FormAutofillNameUtils = {
// Will be loaded from NAME_REFERENCES.
NAME_PREFIXES: [],
NAME_SUFFIXES: [],
FAMILY_NAME_PREFIXES: [],
COMMON_CJK_MULTI_CHAR_SURNAMES: [],
@@ -199,20 +201,17 @@ var FormAutofillNameUtils = {
return nameParts;
},
init() {
if (this._dataLoaded) {
return;
}
- let sandbox = {};
- let scriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
- .getService(Ci.mozIJSSubScriptLoader);
- scriptLoader.loadSubScript(NAME_REFERENCES, sandbox, "utf-8");
+ let sandbox = FormAutofillUtils.loadDataFromScript(NAME_REFERENCES);
Object.assign(this, sandbox.nameReferences);
this._dataLoaded = true;
this.reCJK = new RegExp("[" + this.CJK_RANGE.join("") + "]", "u");
},
splitName(name) {
let nameTokens = name.trim().split(/[ ,\u3000\u30FB\u00B7]+/);
--- a/browser/extensions/formautofill/FormAutofillUtils.jsm
+++ b/browser/extensions/formautofill/FormAutofillUtils.jsm
@@ -3,16 +3,18 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["FormAutofillUtils"];
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+const ADDRESS_REFERENCES = "chrome://formautofill/content/addressReferences.js";
+
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
this.FormAutofillUtils = {
_fieldNameInfo: {
"name": "name",
"given-name": "name",
"additional-name": "name",
"family-name": "name",
@@ -27,16 +29,17 @@ this.FormAutofillUtils = {
"country": "address",
"tel": "tel",
"email": "email",
"cc-name": "creditCard",
"cc-number": "creditCard",
"cc-exp-month": "creditCard",
"cc-exp-year": "creditCard",
},
+ _addressDataLoaded: false,
isAddressField(fieldName) {
return !!this._fieldNameInfo[fieldName] && !this.isCreditCardField(fieldName);
},
isCreditCardField(fieldName) {
return this._fieldNameInfo[fieldName] == "creditCard";
},
@@ -154,13 +157,122 @@ this.FormAutofillUtils = {
log.debug("Label found in input's parent or ancestor.");
return [parent];
}
parent = parent.parentNode;
} while (parent);
return [];
},
+
+ loadDataFromScript(url, sandbox = {}) {
+ let scriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"]
+ .getService(Ci.mozIJSSubScriptLoader);
+ scriptLoader.loadSubScript(url, sandbox, "utf-8");
+ return sandbox;
+ },
+
+ /**
+ * Find the option element from select element.
+ * 1. Try to find the locale using the country from profile.
+ * 2. First pass try to find exact match.
+ * 3. Second pass try to identify values from profile value and options,
+ * and look for a match.
+ * @param {DOMElement} selectEl
+ * @param {object} profile
+ * @param {string} fieldName
+ * @returns {DOMElement}
+ */
+ findSelectOption(selectEl, profile, fieldName) {
+ let value = profile[fieldName];
+ if (!value) {
+ return null;
+ }
+
+ // Load the addressData if needed
+ if (!this._addressDataLoaded) {
+ Object.assign(this, this.loadDataFromScript(ADDRESS_REFERENCES));
+ this._addressDataLoaded = true;
+ }
+
+ // Set dataset to "data/US" as fallback
+ let dataset = this.addressData[`data/${profile.country}`] ||
+ this.addressData["data/US"];
+ let collator = new Intl.Collator(dataset.lang, {sensitivity: "base", ignorePunctuation: true});
+
+ for (let option of selectEl.options) {
+ if (this.strCompare(value, option.value, collator) ||
+ this.strCompare(value, option.text, collator)) {
+ return option;
+ }
+ }
+
+ if (fieldName === "address-level1") {
+ if (!Array.isArray(dataset.sub_keys)) {
+ dataset.sub_keys = dataset.sub_keys.split("~");
+ }
+ if (!Array.isArray(dataset.sub_names)) {
+ dataset.sub_names = dataset.sub_names.split("~");
+ }
+ let keys = dataset.sub_keys;
+ let names = dataset.sub_names;
+ let identifiedValue = this.identifyValue(keys, names, value, collator);
+
+ // No point going any further if we cannot identify value from profile
+ if (identifiedValue === undefined) {
+ return null;
+ }
+
+ // Go through options one by one to find a match.
+ // Also check if any option contain the address-level1 key.
+ let pattern = new RegExp(`\\b${identifiedValue}\\b`, "i");
+ for (let option of selectEl.options) {
+ let optionValue = this.identifyValue(keys, names, option.value, collator);
+ let optionText = this.identifyValue(keys, names, option.text, collator);
+ if (identifiedValue === optionValue || identifiedValue === optionText || pattern.test(option.value)) {
+ return option;
+ }
+ }
+ }
+
+ if (fieldName === "country") {
+ // TODO: Support matching countries (Bug 1375382)
+ }
+
+ return null;
+ },
+
+ /**
+ * Try to match value with keys and names, but always return the key.
+ * @param {array<string>} keys
+ * @param {array<string>} names
+ * @param {string} value
+ * @param {object} collator
+ * @returns {string}
+ */
+ identifyValue(keys, names, value, collator) {
+ let resultKey = keys.find(key => this.strCompare(value, key, collator));
+ if (resultKey) {
+ return resultKey;
+ }
+
+ let index = names.findIndex(name => this.strCompare(value, name, collator));
+ if (index !== -1) {
+ return keys[index];
+ }
+
+ return null;
+ },
+
+ /**
+ * Compare if two strings are the same.
+ * @param {string} a
+ * @param {string} b
+ * @param {object} collator
+ * @returns {boolean}
+ */
+ strCompare(a = "", b = "", collator) {
+ return !collator.compare(a, b);
+ },
};
this.log = null;
this.FormAutofillUtils.defineLazyLogGetter(this, this.EXPORTED_SYMBOLS[0]);
-
new file mode 100644
--- /dev/null
+++ b/browser/extensions/formautofill/content/addressReferences.js
@@ -0,0 +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/. */
+
+/* exported addressData */
+/* eslint max-len: 0 */
+
+"use strict";
+
+// The data below is initially copied from
+// https://chromium-i18n.appspot.com/ssl-aggregate-address
+
+var addressData = {
+ "data/US": {"lang": "en", "upper": "CS", "sub_zipexs": "35000,36999~99500,99999~96799~85000,86999~71600,72999~34000,34099~09000,09999~96200,96699~90000,96199~80000,81999~06000,06999~19700,19999~20000,56999~32000,34999~30000,39901~96910,96932~96700,96899~83200,83999~60000,62999~46000,47999~50000,52999~66000,67999~40000,42799~70000,71599~03900,04999~96960,96979~20600,21999~01000,05544~48000,49999~96941,96944~55000,56799~38600,39799~63000,65999~59000,59999~68000,69999~88900,89999~03000,03899~07000,08999~87000,88499~10000,00544~27000,28999~58000,58999~96950,96952~43000,45999~73000,74999~97000,97999~96940~15000,19699~00600,00999~02800,02999~29000,29999~57000,57999~37000,38599~75000,73344~84000,84999~05000,05999~00800,00899~20100,24699~98000,99499~24700,26999~53000,54999~82000,83414", "zipex": "95014,22162-1010", "name": "UNITED STATES", "zip": "(\\d{5})(?:[ \\-](\\d{4}))?", "zip_name_type": "zip", "fmt": "%N%n%O%n%A%n%C, %S %Z", "state_name_type": "state", "id": "data/US", "languages": "en", "sub_keys": "AL~AK~AS~AZ~AR~AA~AE~AP~CA~CO~CT~DE~DC~FL~GA~GU~HI~ID~IL~IN~IA~KS~KY~LA~ME~MH~MD~MA~MI~FM~MN~MS~MO~MT~NE~NV~NH~NJ~NM~NY~NC~ND~MP~OH~OK~OR~PW~PA~PR~RI~SC~SD~TN~TX~UT~VT~VI~VA~WA~WV~WI~WY", "key": "US", "posturl": "https://tools.usps.com/go/ZipLookupAction!input.action", "require": "ACSZ", "sub_names": "Alabama~Alaska~American Samoa~Arizona~Arkansas~Armed Forces (AA)~Armed Forces (AE)~Armed Forces (AP)~California~Colorado~Connecticut~Delaware~District of Columbia~Florida~Georgia~Guam~Hawaii~Idaho~Illinois~Indiana~Iowa~Kansas~Kentucky~Louisiana~Maine~Marshall Islands~Maryland~Massachusetts~Michigan~Micronesia~Minnesota~Mississippi~Missouri~Montana~Nebraska~Nevada~New Hampshire~New Jersey~New Mexico~New York~North Carolina~North Dakota~Northern Mariana Islands~Ohio~Oklahoma~Oregon~Palau~Pennsylvania~Puerto Rico~Rhode Island~South Carolina~South Dakota~Tennessee~Texas~Utah~Vermont~Virgin Islands~Virginia~Washington~West Virginia~Wisconsin~Wyoming", "sub_zips": "3[56]~99[5-9]~96799~8[56]~71[6-9]|72~340~09~96[2-6]~9[0-5]|96[01]~8[01]~06~19[7-9]~20[02-5]|569~3[23]|34[1-9]~3[01]|398|39901~969([1-2]\\d|3[12])~967[0-8]|9679[0-8]|968~83[2-9]~6[0-2]~4[67]~5[0-2]~6[67]~4[01]|42[0-7]~70|71[0-5]~039|04~969[67]~20[6-9]|21~01|02[0-7]|05501|05544~4[89]~9694[1-4]~55|56[0-7]~38[6-9]|39[0-7]~6[3-5]~59~6[89]~889|89~03[0-8]~0[78]~87|88[0-4]~1[0-4]|06390|00501|00544~2[78]~58~9695[0-2]~4[3-5]~7[34]~97~969(39|40)~1[5-8]|19[0-6]~00[679]~02[89]~29~57~37|38[0-5]~7[5-9]|885|73301|73344~84~05~008~201|2[23]|24[0-6]~98|99[0-4]~24[7-9]|2[56]~5[34]~82|83[01]|83414"},
+};
--- a/browser/extensions/formautofill/test/mochitest/test_basic_autocomplete_form.html
+++ b/browser/extensions/formautofill/test/mochitest/test_basic_autocomplete_form.html
@@ -20,21 +20,23 @@ Form autofill test: simple form address
"use strict";
let expectingPopup = null;
let MOCK_STORAGE = [{
organization: "Sesame Street",
"street-address": "123 Sesame Street.",
tel: "1-345-345-3456",
country: "US",
+ "address-level1": "NY",
}, {
organization: "Mozilla",
"street-address": "331 E. Evelyn Avenue",
tel: "1-650-903-0800",
country: "US",
+ "address-level1": "CA",
}];
function expectPopup() {
info("expecting a popup");
return new Promise(resolve => {
expectingPopup = resolve;
});
}
@@ -188,16 +190,22 @@ registerPopupShownListener(popupShownLis
<p>This is a basic form.</p>
<p><label>organization: <input id="organization" name="organization" autocomplete="organization" type="text"></label></p>
<p><label>streetAddress: <input id="street-address" name="street-address" autocomplete="street-address" type="text"></label></p>
<p><label>tel: <input id="tel" name="tel" autocomplete="tel" type="text"></label></p>
<p><label>email: <input id="email" name="email" autocomplete="email" type="text"></label></p>
<p><label>country: <select id="country" name="country" autocomplete="country">
<option/>
<option value="US">United States</option>
- </label></p>
+ </select></label></p>
+ <p><label>states: <select id="address-level1" name="address-level1" autocomplete="address-level1">
+ <option/>
+ <option value="CA">California</option>
+ <option value="NY">New York</option>
+ <option value="WA">Washington</option>
+ </select></label></p>
</form>
</div>
<pre id="test"></pre>
</body>
</html>
--- a/browser/extensions/formautofill/test/unit/test_autofillFormFields.js
+++ b/browser/extensions/formautofill/test/unit/test_autofillFormFields.js
@@ -245,16 +245,73 @@ const TESTCASES_INPUT_UNCHANGED = [
},
expectedResult: {
"country": "US",
"state": "",
},
},
];
+const TESTCASES_US_STATES = [
+ {
+ description: "Form with US states select elements; with lower case state key",
+ document: `<form><select id="state" autocomplete="shipping address-level1">
+ <option value=""></option>
+ <option value="CA">California</option>
+ </select></form>`,
+ fieldDetails: [
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level1", "element": {}},
+ ],
+ profileData: {
+ "guid": "123",
+ "country": "US",
+ "address-level1": "ca",
+ },
+ expectedResult: {
+ "state": "CA",
+ },
+ },
+ {
+ description: "Form with US states select elements; with state name and extra spaces",
+ document: `<form><select id="state" autocomplete="shipping address-level1">
+ <option value=""></option>
+ <option value="CA">CA</option>
+ </select></form>`,
+ fieldDetails: [
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level1", "element": {}},
+ ],
+ profileData: {
+ "guid": "123",
+ "country": "US",
+ "address-level1": " California ",
+ },
+ expectedResult: {
+ "state": "CA",
+ },
+ },
+ {
+ description: "Form with US states select elements; with partial state key match",
+ document: `<form><select id="state" autocomplete="shipping address-level1">
+ <option value=""></option>
+ <option value="US-WA">WA-Washington</option>
+ </select></form>`,
+ fieldDetails: [
+ {"section": "", "addressType": "shipping", "contactType": "", "fieldName": "address-level1", "element": {}},
+ ],
+ profileData: {
+ "guid": "123",
+ "country": "US",
+ "address-level1": "WA",
+ },
+ expectedResult: {
+ "state": "US-WA",
+ },
+ },
+];
+
function do_test(testcases, testFn) {
for (let tc of testcases) {
(function() {
let testcase = tc;
add_task(async function() {
do_print("Starting testcase: " + testcase.description);
let doc = MockDocument.createTestDocument("http://localhost:8080/test/",
@@ -322,8 +379,21 @@ do_test(TESTCASES_INPUT_UNCHANGED, (test
clearTimeout(timer);
reject(`${event.type} event should not fire`);
};
element.addEventListener("change", cleaner);
element.addEventListener("input", cleaner);
}),
];
});
+
+do_test(TESTCASES_US_STATES, (testcase, element) => {
+ let id = element.id;
+ return [
+ new Promise(resolve => {
+ element.addEventListener("input", () => {
+ Assert.equal(element.value, testcase.expectedResult[id],
+ "Check the " + id + " field was filled with correct data");
+ resolve();
+ }, {once: true});
+ }),
+ ];
+});