diff --git a/browser/extensions/formautofill/FormAutofillUtils.jsm b/browser/extensions/formautofill/FormAutofillUtils.jsm index 11a0740e96db..73ea961b566c 100644 --- a/browser/extensions/formautofill/FormAutofillUtils.jsm +++ b/browser/extensions/formautofill/FormAutofillUtils.jsm @@ -4,17 +4,16 @@ "use strict"; -this.EXPORTED_SYMBOLS = ["FormAutofillUtils"]; +this.EXPORTED_SYMBOLS = ["FormAutofillUtils", "AddressDataLoader"]; const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; -const ADDRESS_REFERENCES = "resource://formautofill/addressmetadata/addressReferences.js"; +const ADDRESS_METADATA_PATH = "resource://formautofill/addressmetadata/"; +const ADDRESS_REFERENCES = "addressReferences.js"; +const ADDRESS_REFERENCES_EXT = "addressReferencesExt.js"; -// TODO: We only support US in MVP. We are going to support more countries in -// bug 1370193. -const ALTERNATIVE_COUNTRY_NAMES = { - "US": ["US", "United States of America", "United States", "America", "U.S.", "USA", "U.S.A.", "U.S.A"], -}; +// TODO: This list should become a pref in Bug 1413494 +const SUPPORTED_COUNTRY_LIST = ["US"]; const ADDRESSES_COLLECTION_NAME = "addresses"; const CREDITCARDS_COLLECTION_NAME = "creditCards"; @@ -43,6 +42,77 @@ const MAX_FIELD_VALUE_LENGTH = 200; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); +let AddressDataLoader = { + // Status of address data loading. We'll load all the countries with basic level 1 + // information while requesting conutry information, and set country to true. + // Level 1 Set is for recording which country's level 1/level 2 data is loaded, + // since we only load this when getCountryAddressData called with level 1 parameter. + _dataLoaded: { + country: false, + level1: new Set(), + }, + /** + * Load address data and extension script into a sandbox from different paths. + * @param {string} path + * The path for address data and extension script. It could be root of the address + * metadata folder(addressmetadata/) or under specific country(addressmetadata/TW/). + * @returns {object} + * A sandbox that contains address data object with properties from extension. + */ + _loadScripts(path) { + let sandbox = {}; + let extSandbox = {}; + + try { + sandbox = FormAutofillUtils.loadDataFromScript(path + ADDRESS_REFERENCES); + extSandbox = FormAutofillUtils.loadDataFromScript(path + ADDRESS_REFERENCES_EXT); + } catch (e) { + // Will return only address references if extension loading failed or empty sandbox if + // address references loading failed. + return sandbox; + } + + if (extSandbox.addressDataExt) { + for (let key in extSandbox.addressDataExt) { + Object.assign(sandbox.addressData[key], extSandbox.addressDataExt[key]); + } + } + return sandbox; + }, + /** + * We'll cache addressData in the loader once the data loaded from scripts. + * It'll become the example below after loading addressReferences with extension: + * addressData: { + "data/US": {"lang": "en", ...// Data defined in libaddressinput metadata + * "alternative_names": ... // Data defined in extension } + * "data/CA": {} // Other supported country metadata + * "data/TW": {} // Other supported country metadata + * "data/TW/台北市": {} // Other supported country level 1 metadata + * } + * @param {string} country + * @param {string} level1 + * @returns {object} + */ + getData(country, level1 = null) { + // Load the addressData if needed + if (!this._dataLoaded.country) { + this._addressData = this._loadScripts(ADDRESS_METADATA_PATH).addressData; + this._dataLoaded.country = true; + } + if (!level1) { + return this._addressData[`data/${country}`]; + } + // If level1 is set, load addressReferences under country folder with specific + // country/level 1 for level 2 information. + if (!this._dataLoaded.level1.has(country)) { + Object.assign(this._addressData, + this._loadScripts(`${ADDRESS_METADATA_PATH}${country}/`).addressData); + this._dataLoaded.level1.add(country); + } + return this._addressData[`data/${country}/${level1}`]; + }, +}; + this.FormAutofillUtils = { get AUTOFILL_FIELDS_THRESHOLD() { return 3; }, get isAutofillEnabled() { return this.isAutofillAddressesEnabled || this.isAutofillCreditCardsEnabled; }, @@ -94,7 +164,7 @@ this.FormAutofillUtils = { "cc-exp-year": "creditCard", "cc-exp": "creditCard", }, - _addressDataLoaded: false, + _collators: {}, _reAlternativeCountryNames: {}, @@ -221,18 +291,15 @@ this.FormAutofillUtils = { return sandbox; }, - /** - * Get country address data. Fallback to US if not found. - * @param {string} country - * @returns {object} - */ - getCountryAddressData(country) { - // Load the addressData if needed - if (!this._addressDataLoaded) { - Object.assign(this, this.loadDataFromScript(ADDRESS_REFERENCES)); - this._addressDataLoaded = true; + // Get country address data and fallback to US if not found. + // See AddressDataLoader.getData for more details of addressData structure. + getCountryAddressData(country, level1 = null) { + let metadata = AddressDataLoader.getData(country, level1); + if (!metadata) { + metadata = level1 ? null : AddressDataLoader.getData("US"); } - return this.addressData[`data/${country}`] || this.addressData["data/US"]; + + return metadata; }, /** @@ -311,12 +378,13 @@ this.FormAutofillUtils = { * @returns {string} The matching country code. */ identifyCountryCode(countryName, countrySpecified) { - let countries = countrySpecified ? [countrySpecified] : Object.keys(ALTERNATIVE_COUNTRY_NAMES); + let countries = countrySpecified ? [countrySpecified] : SUPPORTED_COUNTRY_LIST; for (let country of countries) { let collators = this.getCollators(country); - let alternativeCountryNames = ALTERNATIVE_COUNTRY_NAMES[country]; + let metadata = this.getCountryAddressData(country); + let alternativeCountryNames = metadata.alternative_names || [metadata.name]; let reAlternativeCountryNames = this._reAlternativeCountryNames[country]; if (!reAlternativeCountryNames) { reAlternativeCountryNames = this._reAlternativeCountryNames[country] = []; @@ -445,7 +513,7 @@ this.FormAutofillUtils = { break; } case "country": { - if (ALTERNATIVE_COUNTRY_NAMES[value]) { + if (this.getCountryAddressData(value).alternative_names) { for (let option of selectEl.options) { if (this.identifyCountryCode(option.text, value) || this.identifyCountryCode(option.value, value)) { return option; diff --git a/browser/extensions/formautofill/addressmetadata/addressReferences.js b/browser/extensions/formautofill/addressmetadata/addressReferences.js index 2b2fa0778117..cb7979dc812a 100644 --- a/browser/extensions/formautofill/addressmetadata/addressReferences.js +++ b/browser/extensions/formautofill/addressmetadata/addressReferences.js @@ -10,6 +10,11 @@ // The data below is initially copied from // https://chromium-i18n.appspot.com/ssl-aggregate-address +// WARNING: DO NOT change any value or add additional properties in addressData. +// We only accept the metadata of the supported countries that is copied from libaddressinput directly. +// Please edit addressReferencesExt.js instead if you want to add new property as complement +// or overwrite the existing properties. + 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"}, }; diff --git a/browser/extensions/formautofill/addressmetadata/addressReferencesExt.js b/browser/extensions/formautofill/addressmetadata/addressReferencesExt.js new file mode 100644 index 000000000000..bfbe5fffe3d7 --- /dev/null +++ b/browser/extensions/formautofill/addressmetadata/addressReferencesExt.js @@ -0,0 +1,17 @@ +/* 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 addressDataExt */ +/* eslint max-len: 0 */ + +"use strict"; + +// "addressDataExt" uses the same key as "addressData" in "addressReferences.js" and contains +// contains the information we need but absent in "libaddressinput" such as alternative names. + +// TODO: We only support the alternative name of US in MVP. We are going to support more countries in +// bug 1370193. +var addressDataExt = { + "data/US": {"alternative_names": ["US", "United States of America", "United States", "America", "U.S.", "USA", "U.S.A.", "U.S.A"]}, +}; diff --git a/browser/extensions/formautofill/test/unit/test_addressDataLoader.js b/browser/extensions/formautofill/test/unit/test_addressDataLoader.js new file mode 100644 index 000000000000..6b4a87ee2c5b --- /dev/null +++ b/browser/extensions/formautofill/test/unit/test_addressDataLoader.js @@ -0,0 +1,49 @@ +"use strict"; + +Cu.import("resource://formautofill/FormAutofillUtils.jsm"); + +add_task(async function test_initalState() { + // addressData should not exist + Assert.equal(AddressDataLoader._addressData, undefined); + // Verify _dataLoaded state + Assert.equal(AddressDataLoader._dataLoaded.country, false); + Assert.equal(AddressDataLoader._dataLoaded.level1.size, 0); +}); + +add_task(async function test_loadDataState() { + sinon.spy(AddressDataLoader, "_loadScripts"); + let metadata = FormAutofillUtils.getCountryAddressData("US"); + Assert.ok(AddressDataLoader._addressData, "addressData exists"); + // Verify _dataLoaded state + Assert.equal(AddressDataLoader._dataLoaded.country, true); + Assert.equal(AddressDataLoader._dataLoaded.level1.size, 0); + // _loadScripts should be called + sinon.assert.called(AddressDataLoader._loadScripts); + // Verify metadata + Assert.equal(metadata.id, "data/US"); + Assert.ok(metadata.alternative_names, + "US alternative names should be loaded from extension"); + AddressDataLoader._loadScripts.reset(); + + // Load data without country + let newMetadata = FormAutofillUtils.getCountryAddressData(); + // _loadScripts should not be called + sinon.assert.notCalled(AddressDataLoader._loadScripts); + Assert.deepEqual(metadata, newMetadata, "metadata should be US if country is not specified"); + AddressDataLoader._loadScripts.reset(); + + // Load level 1 data that does not exist + let undefinedMetadata = FormAutofillUtils.getCountryAddressData("US", "CA"); + // _loadScripts should be called + sinon.assert.called(AddressDataLoader._loadScripts); + Assert.equal(undefinedMetadata, undefined, "metadata should be undefined"); + Assert.ok(AddressDataLoader._dataLoaded.level1.has("US"), + "level 1 state array should be set even there's no valid metadata"); + AddressDataLoader._loadScripts.reset(); + + // Load level 1 data again + undefinedMetadata = FormAutofillUtils.getCountryAddressData("US", "AS"); + Assert.equal(undefinedMetadata, undefined, "metadata should be undefined"); + // _loadScripts should not be called + sinon.assert.notCalled(AddressDataLoader._loadScripts); +}); diff --git a/browser/extensions/formautofill/test/unit/xpcshell.ini b/browser/extensions/formautofill/test/unit/xpcshell.ini index 1f898038c015..a10495903995 100644 --- a/browser/extensions/formautofill/test/unit/xpcshell.ini +++ b/browser/extensions/formautofill/test/unit/xpcshell.ini @@ -18,6 +18,7 @@ support-files = [heuristics/third_party/test_Staples.js] [heuristics/third_party/test_Walmart.js] [test_activeStatus.js] +[test_addressDataLoader.js] [test_addressRecords.js] [test_autofillFormFields.js] [test_collectFormFields.js] diff --git a/tools/lint/eslint/modules.json b/tools/lint/eslint/modules.json index 81c698fb150d..b1cc8584c139 100644 --- a/tools/lint/eslint/modules.json +++ b/tools/lint/eslint/modules.json @@ -72,6 +72,7 @@ "forms.jsm": ["FormData"], "FormAutofillHeuristics.jsm": ["FormAutofillHeuristics", "LabelUtils"], "FormAutofillSync.jsm": ["AddressesEngine", "CreditCardsEngine"], + "FormAutofillUtils.jsm": ["FormAutofillUtils", "AddressDataLoader"], "FrameScriptManager.jsm": ["getNewLoaderID"], "fxa_utils.js": ["initializeIdentityWithTokenServerResponse"], "fxaccounts.jsm": ["Authentication"],