1/* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5"use strict"; 6 7var EXPORTED_SYMBOLS = ["FormAutofillUtils", "AddressDataLoader"]; 8 9const ADDRESS_METADATA_PATH = "resource://autofill/addressmetadata/"; 10const ADDRESS_REFERENCES = "addressReferences.js"; 11const ADDRESS_REFERENCES_EXT = "addressReferencesExt.js"; 12 13const ADDRESSES_COLLECTION_NAME = "addresses"; 14const CREDITCARDS_COLLECTION_NAME = "creditCards"; 15const MANAGE_ADDRESSES_KEYWORDS = [ 16 "manageAddressesTitle", 17 "addNewAddressTitle", 18]; 19const EDIT_ADDRESS_KEYWORDS = [ 20 "givenName", 21 "additionalName", 22 "familyName", 23 "organization2", 24 "streetAddress", 25 "state", 26 "province", 27 "city", 28 "country", 29 "zip", 30 "postalCode", 31 "email", 32 "tel", 33]; 34const MANAGE_CREDITCARDS_KEYWORDS = [ 35 "manageCreditCardsTitle", 36 "addNewCreditCardTitle", 37]; 38const EDIT_CREDITCARD_KEYWORDS = [ 39 "cardNumber", 40 "nameOnCard", 41 "cardExpiresMonth", 42 "cardExpiresYear", 43 "cardNetwork", 44]; 45const FIELD_STATES = { 46 NORMAL: "NORMAL", 47 AUTO_FILLED: "AUTO_FILLED", 48 PREVIEW: "PREVIEW", 49}; 50const SECTION_TYPES = { 51 ADDRESS: "address", 52 CREDIT_CARD: "creditCard", 53}; 54 55// The maximum length of data to be saved in a single field for preventing DoS 56// attacks that fill the user's hard drive(s). 57const MAX_FIELD_VALUE_LENGTH = 200; 58 59const { XPCOMUtils } = ChromeUtils.import( 60 "resource://gre/modules/XPCOMUtils.jsm" 61); 62const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); 63const { FormAutofill } = ChromeUtils.import( 64 "resource://autofill/FormAutofill.jsm" 65); 66XPCOMUtils.defineLazyModuleGetters(this, { 67 CreditCard: "resource://gre/modules/CreditCard.jsm", 68 OSKeyStore: "resource://gre/modules/OSKeyStore.jsm", 69}); 70 71let AddressDataLoader = { 72 // Status of address data loading. We'll load all the countries with basic level 1 73 // information while requesting conutry information, and set country to true. 74 // Level 1 Set is for recording which country's level 1/level 2 data is loaded, 75 // since we only load this when getCountryAddressData called with level 1 parameter. 76 _dataLoaded: { 77 country: false, 78 level1: new Set(), 79 }, 80 81 /** 82 * Load address data and extension script into a sandbox from different paths. 83 * @param {string} path 84 * The path for address data and extension script. It could be root of the address 85 * metadata folder(addressmetadata/) or under specific country(addressmetadata/TW/). 86 * @returns {object} 87 * A sandbox that contains address data object with properties from extension. 88 */ 89 _loadScripts(path) { 90 let sandbox = {}; 91 let extSandbox = {}; 92 93 try { 94 sandbox = FormAutofillUtils.loadDataFromScript(path + ADDRESS_REFERENCES); 95 extSandbox = FormAutofillUtils.loadDataFromScript( 96 path + ADDRESS_REFERENCES_EXT 97 ); 98 } catch (e) { 99 // Will return only address references if extension loading failed or empty sandbox if 100 // address references loading failed. 101 return sandbox; 102 } 103 104 if (extSandbox.addressDataExt) { 105 for (let key in extSandbox.addressDataExt) { 106 let addressDataForKey = sandbox.addressData[key]; 107 if (!addressDataForKey) { 108 addressDataForKey = sandbox.addressData[key] = {}; 109 } 110 111 Object.assign(addressDataForKey, extSandbox.addressDataExt[key]); 112 } 113 } 114 return sandbox; 115 }, 116 117 /** 118 * Convert certain properties' string value into array. We should make sure 119 * the cached data is parsed. 120 * @param {object} data Original metadata from addressReferences. 121 * @returns {object} parsed metadata with property value that converts to array. 122 */ 123 _parse(data) { 124 if (!data) { 125 return null; 126 } 127 128 const properties = [ 129 "languages", 130 "sub_keys", 131 "sub_isoids", 132 "sub_names", 133 "sub_lnames", 134 ]; 135 for (let key of properties) { 136 if (!data[key]) { 137 continue; 138 } 139 // No need to normalize data if the value is array already. 140 if (Array.isArray(data[key])) { 141 return data; 142 } 143 144 data[key] = data[key].split("~"); 145 } 146 return data; 147 }, 148 149 /** 150 * We'll cache addressData in the loader once the data loaded from scripts. 151 * It'll become the example below after loading addressReferences with extension: 152 * addressData: { 153 * "data/US": {"lang": ["en"], ...// Data defined in libaddressinput metadata 154 * "alternative_names": ... // Data defined in extension } 155 * "data/CA": {} // Other supported country metadata 156 * "data/TW": {} // Other supported country metadata 157 * "data/TW/台北市": {} // Other supported country level 1 metadata 158 * } 159 * @param {string} country 160 * @param {string?} level1 161 * @returns {object} Default locale metadata 162 */ 163 _loadData(country, level1 = null) { 164 // Load the addressData if needed 165 if (!this._dataLoaded.country) { 166 this._addressData = this._loadScripts(ADDRESS_METADATA_PATH).addressData; 167 this._dataLoaded.country = true; 168 } 169 if (!level1) { 170 return this._parse(this._addressData[`data/${country}`]); 171 } 172 // If level1 is set, load addressReferences under country folder with specific 173 // country/level 1 for level 2 information. 174 if (!this._dataLoaded.level1.has(country)) { 175 Object.assign( 176 this._addressData, 177 this._loadScripts(`${ADDRESS_METADATA_PATH}${country}/`).addressData 178 ); 179 this._dataLoaded.level1.add(country); 180 } 181 return this._parse(this._addressData[`data/${country}/${level1}`]); 182 }, 183 184 /** 185 * Return the region metadata with default locale and other locales (if exists). 186 * @param {string} country 187 * @param {string?} level1 188 * @returns {object} Return default locale and other locales metadata. 189 */ 190 getData(country, level1 = null) { 191 let defaultLocale = this._loadData(country, level1); 192 if (!defaultLocale) { 193 return null; 194 } 195 196 let countryData = this._parse(this._addressData[`data/${country}`]); 197 let locales = []; 198 // TODO: Should be able to support multi-locale level 1/ level 2 metadata query 199 // in Bug 1421886 200 if (countryData.languages) { 201 let list = countryData.languages.filter(key => key !== countryData.lang); 202 locales = list.map(key => 203 this._parse(this._addressData[`${defaultLocale.id}--${key}`]) 204 ); 205 } 206 return { defaultLocale, locales }; 207 }, 208}; 209 210this.FormAutofillUtils = { 211 get AUTOFILL_FIELDS_THRESHOLD() { 212 return 3; 213 }, 214 215 ADDRESSES_COLLECTION_NAME, 216 CREDITCARDS_COLLECTION_NAME, 217 MANAGE_ADDRESSES_KEYWORDS, 218 EDIT_ADDRESS_KEYWORDS, 219 MANAGE_CREDITCARDS_KEYWORDS, 220 EDIT_CREDITCARD_KEYWORDS, 221 MAX_FIELD_VALUE_LENGTH, 222 FIELD_STATES, 223 SECTION_TYPES, 224 225 _fieldNameInfo: { 226 name: "name", 227 "given-name": "name", 228 "additional-name": "name", 229 "family-name": "name", 230 organization: "organization", 231 "street-address": "address", 232 "address-line1": "address", 233 "address-line2": "address", 234 "address-line3": "address", 235 "address-level1": "address", 236 "address-level2": "address", 237 "postal-code": "address", 238 country: "address", 239 "country-name": "address", 240 tel: "tel", 241 "tel-country-code": "tel", 242 "tel-national": "tel", 243 "tel-area-code": "tel", 244 "tel-local": "tel", 245 "tel-local-prefix": "tel", 246 "tel-local-suffix": "tel", 247 "tel-extension": "tel", 248 email: "email", 249 "cc-name": "creditCard", 250 "cc-given-name": "creditCard", 251 "cc-additional-name": "creditCard", 252 "cc-family-name": "creditCard", 253 "cc-number": "creditCard", 254 "cc-exp-month": "creditCard", 255 "cc-exp-year": "creditCard", 256 "cc-exp": "creditCard", 257 "cc-type": "creditCard", 258 }, 259 260 _collators: {}, 261 _reAlternativeCountryNames: {}, 262 263 isAddressField(fieldName) { 264 return ( 265 !!this._fieldNameInfo[fieldName] && !this.isCreditCardField(fieldName) 266 ); 267 }, 268 269 isCreditCardField(fieldName) { 270 return this._fieldNameInfo[fieldName] == "creditCard"; 271 }, 272 273 isCCNumber(ccNumber) { 274 return CreditCard.isValidNumber(ccNumber); 275 }, 276 277 ensureLoggedIn(promptMessage) { 278 return OSKeyStore.ensureLoggedIn( 279 this._reauthEnabledByUser && promptMessage ? promptMessage : false 280 ); 281 }, 282 283 /** 284 * Get the array of credit card network ids ("types") we expect and offer as valid choices 285 * 286 * @returns {Array} 287 */ 288 getCreditCardNetworks() { 289 return CreditCard.SUPPORTED_NETWORKS; 290 }, 291 292 getCategoryFromFieldName(fieldName) { 293 return this._fieldNameInfo[fieldName]; 294 }, 295 296 getCategoriesFromFieldNames(fieldNames) { 297 let categories = new Set(); 298 for (let fieldName of fieldNames) { 299 let info = this.getCategoryFromFieldName(fieldName); 300 if (info) { 301 categories.add(info); 302 } 303 } 304 return Array.from(categories); 305 }, 306 307 getAddressSeparator() { 308 // The separator should be based on the L10N address format, and using a 309 // white space is a temporary solution. 310 return " "; 311 }, 312 313 /** 314 * Get address display label. It should display information separated 315 * by a comma. 316 * 317 * @param {object} address 318 * @param {string?} addressFields Override the fields which can be displayed, but not the order. 319 * @returns {string} 320 */ 321 getAddressLabel(address, addressFields = null) { 322 // TODO: Implement a smarter way for deciding what to display 323 // as option text. Possibly improve the algorithm in 324 // ProfileAutoCompleteResult.jsm and reuse it here. 325 let fieldOrder = [ 326 "name", 327 "-moz-street-address-one-line", // Street address 328 "address-level3", // Townland / Neighborhood / Village 329 "address-level2", // City/Town 330 "organization", // Company or organization name 331 "address-level1", // Province/State (Standardized code if possible) 332 "country-name", // Country name 333 "postal-code", // Postal code 334 "tel", // Phone number 335 "email", // Email address 336 ]; 337 338 address = { ...address }; 339 let parts = []; 340 if (addressFields) { 341 let requiredFields = addressFields.trim().split(/\s+/); 342 fieldOrder = fieldOrder.filter(name => requiredFields.includes(name)); 343 } 344 if (address["street-address"]) { 345 address["-moz-street-address-one-line"] = this.toOneLineAddress( 346 address["street-address"] 347 ); 348 } 349 for (const fieldName of fieldOrder) { 350 let string = address[fieldName]; 351 if (string) { 352 parts.push(string); 353 } 354 if (parts.length == 2 && !addressFields) { 355 break; 356 } 357 } 358 return parts.join(", "); 359 }, 360 361 /** 362 * Internal method to split an address to multiple parts per the provided delimiter, 363 * removing blank parts. 364 * @param {string} address The address the split 365 * @param {string} [delimiter] The separator that is used between lines in the address 366 * @returns {string[]} 367 */ 368 _toStreetAddressParts(address, delimiter = "\n") { 369 let array = typeof address == "string" ? address.split(delimiter) : address; 370 371 if (!Array.isArray(array)) { 372 return []; 373 } 374 return array.map(s => (s ? s.trim() : "")).filter(s => s); 375 }, 376 377 /** 378 * Converts a street address to a single line, removing linebreaks marked by the delimiter 379 * @param {string} address The address the convert 380 * @param {string} [delimiter] The separator that is used between lines in the address 381 * @returns {string} 382 */ 383 toOneLineAddress(address, delimiter = "\n") { 384 let addressParts = this._toStreetAddressParts(address, delimiter); 385 return addressParts.join(this.getAddressSeparator()); 386 }, 387 388 /** 389 * Compares two addresses, removing internal whitespace 390 * @param {string} a The first address to compare 391 * @param {string} b The second address to compare 392 * @param {array} collators Search collators that will be used for comparison 393 * @param {string} [delimiter="\n"] The separator that is used between lines in the address 394 * @returns {boolean} True if the addresses are equal, false otherwise 395 */ 396 compareStreetAddress(a, b, collators, delimiter = "\n") { 397 let oneLineA = this._toStreetAddressParts(a, delimiter) 398 .map(p => p.replace(/\s/g, "")) 399 .join(""); 400 let oneLineB = this._toStreetAddressParts(b, delimiter) 401 .map(p => p.replace(/\s/g, "")) 402 .join(""); 403 return this.strCompare(oneLineA, oneLineB, collators); 404 }, 405 406 /** 407 * In-place concatenate tel-related components into a single "tel" field and 408 * delete unnecessary fields. 409 * @param {object} address An address record. 410 */ 411 compressTel(address) { 412 let telCountryCode = address["tel-country-code"] || ""; 413 let telAreaCode = address["tel-area-code"] || ""; 414 415 if (!address.tel) { 416 if (address["tel-national"]) { 417 address.tel = telCountryCode + address["tel-national"]; 418 } else if (address["tel-local"]) { 419 address.tel = telCountryCode + telAreaCode + address["tel-local"]; 420 } else if (address["tel-local-prefix"] && address["tel-local-suffix"]) { 421 address.tel = 422 telCountryCode + 423 telAreaCode + 424 address["tel-local-prefix"] + 425 address["tel-local-suffix"]; 426 } 427 } 428 429 for (let field in address) { 430 if (field != "tel" && this.getCategoryFromFieldName(field) == "tel") { 431 delete address[field]; 432 } 433 } 434 }, 435 436 autofillFieldSelector(doc) { 437 return doc.querySelectorAll("input, select"); 438 }, 439 440 /** 441 * Determines if an element is visually hidden or not. 442 * 443 * NOTE: this does not encompass every possible way of hiding an element. 444 * Instead, we check some of the more common methods of hiding for performance reasons. 445 * See Bug 1727832 for follow up. 446 * @param {HTMLElement} element 447 * @returns {boolean} 448 */ 449 isFieldVisible(element) { 450 if (element.hidden) { 451 return false; 452 } 453 if (element.style.display == "none") { 454 return false; 455 } 456 return true; 457 }, 458 459 ALLOWED_TYPES: ["text", "email", "tel", "number", "month"], 460 isFieldEligibleForAutofill(element) { 461 let tagName = element.tagName; 462 if (tagName == "INPUT") { 463 // `element.type` can be recognized as `text`, if it's missing or invalid. 464 if (!this.ALLOWED_TYPES.includes(element.type)) { 465 return false; 466 } 467 // If the field is visually invisible, we do not want to autofill into it. 468 if (!this.isFieldVisible(element)) { 469 return false; 470 } 471 } else if (tagName != "SELECT") { 472 return false; 473 } 474 475 return true; 476 }, 477 478 loadDataFromScript(url, sandbox = {}) { 479 Services.scriptloader.loadSubScript(url, sandbox); 480 return sandbox; 481 }, 482 483 /** 484 * Get country address data and fallback to US if not found. 485 * See AddressDataLoader._loadData for more details of addressData structure. 486 * @param {string} [country=FormAutofill.DEFAULT_REGION] 487 * The country code for requesting specific country's metadata. It'll be 488 * default region if parameter is not set. 489 * @param {string} [level1=null] 490 * Return address level 1/level 2 metadata if parameter is set. 491 * @returns {object|null} 492 * Return metadata of specific region with default locale and other supported 493 * locales. We need to return a default country metadata for layout format 494 * and collator, but for sub-region metadata we'll just return null if not found. 495 */ 496 getCountryAddressRawData( 497 country = FormAutofill.DEFAULT_REGION, 498 level1 = null 499 ) { 500 let metadata = AddressDataLoader.getData(country, level1); 501 if (!metadata) { 502 if (level1) { 503 return null; 504 } 505 // Fallback to default region if we couldn't get data from given country. 506 if (country != FormAutofill.DEFAULT_REGION) { 507 metadata = AddressDataLoader.getData(FormAutofill.DEFAULT_REGION); 508 } 509 } 510 511 // TODO: Now we fallback to US if we couldn't get data from default region, 512 // but it could be removed in bug 1423464 if it's not necessary. 513 if (!metadata) { 514 metadata = AddressDataLoader.getData("US"); 515 } 516 return metadata; 517 }, 518 519 /** 520 * Get country address data with default locale. 521 * @param {string} country 522 * @param {string} level1 523 * @returns {object|null} Return metadata of specific region with default locale. 524 * NOTE: The returned data may be for a default region if the 525 * specified one cannot be found. Callers who only want the specific 526 * region should check the returned country code. 527 */ 528 getCountryAddressData(country, level1) { 529 let metadata = this.getCountryAddressRawData(country, level1); 530 return metadata && metadata.defaultLocale; 531 }, 532 533 /** 534 * Get country address data with all locales. 535 * @param {string} country 536 * @param {string} level1 537 * @returns {array<object>|null} 538 * Return metadata of specific region with all the locales. 539 * NOTE: The returned data may be for a default region if the 540 * specified one cannot be found. Callers who only want the specific 541 * region should check the returned country code. 542 */ 543 getCountryAddressDataWithLocales(country, level1) { 544 let metadata = this.getCountryAddressRawData(country, level1); 545 return metadata && [metadata.defaultLocale, ...metadata.locales]; 546 }, 547 548 /** 549 * Get the collators based on the specified country. 550 * @param {string} country The specified country. 551 * @returns {array} An array containing several collator objects. 552 */ 553 getSearchCollators(country) { 554 // TODO: Only one language should be used at a time per country. The locale 555 // of the page should be taken into account to do this properly. 556 // We are going to support more countries in bug 1370193 and this 557 // should be addressed when we start to implement that bug. 558 559 if (!this._collators[country]) { 560 let dataset = this.getCountryAddressData(country); 561 let languages = dataset.languages || [dataset.lang]; 562 let options = { 563 ignorePunctuation: true, 564 sensitivity: "base", 565 usage: "search", 566 }; 567 this._collators[country] = languages.map( 568 lang => new Intl.Collator(lang, options) 569 ); 570 } 571 return this._collators[country]; 572 }, 573 574 // Based on the list of fields abbreviations in 575 // https://github.com/googlei18n/libaddressinput/wiki/AddressValidationMetadata 576 FIELDS_LOOKUP: { 577 N: "name", 578 O: "organization", 579 A: "street-address", 580 S: "address-level1", 581 C: "address-level2", 582 D: "address-level3", 583 Z: "postal-code", 584 n: "newLine", 585 }, 586 587 /** 588 * Parse a country address format string and outputs an array of fields. 589 * Spaces, commas, and other literals are ignored in this implementation. 590 * For example, format string "%A%n%C, %S" should return: 591 * [ 592 * {fieldId: "street-address", newLine: true}, 593 * {fieldId: "address-level2"}, 594 * {fieldId: "address-level1"}, 595 * ] 596 * 597 * @param {string} fmt Country address format string 598 * @returns {array<object>} List of fields 599 */ 600 parseAddressFormat(fmt) { 601 if (!fmt) { 602 throw new Error("fmt string is missing."); 603 } 604 605 return fmt.match(/%[^%]/g).reduce((parsed, part) => { 606 // Take the first letter of each segment and try to identify it 607 let fieldId = this.FIELDS_LOOKUP[part[1]]; 608 // Early return if cannot identify part. 609 if (!fieldId) { 610 return parsed; 611 } 612 // If a new line is detected, add an attribute to the previous field. 613 if (fieldId == "newLine") { 614 let size = parsed.length; 615 if (size) { 616 parsed[size - 1].newLine = true; 617 } 618 return parsed; 619 } 620 return parsed.concat({ fieldId }); 621 }, []); 622 }, 623 624 /** 625 * Used to populate dropdowns in the UI (e.g. FormAutofill preferences). 626 * Use findAddressSelectOption for matching a value to a region. 627 * 628 * @param {string[]} subKeys An array of regionCode strings 629 * @param {string[]} subIsoids An array of ISO ID strings, if provided will be preferred over the key 630 * @param {string[]} subNames An array of regionName strings 631 * @param {string[]} subLnames An array of latinised regionName strings 632 * @returns {Map?} Returns null if subKeys or subNames are not truthy. 633 * Otherwise, a Map will be returned mapping keys -> names. 634 */ 635 buildRegionMapIfAvailable(subKeys, subIsoids, subNames, subLnames) { 636 // Not all regions have sub_keys. e.g. DE 637 if ( 638 !subKeys || 639 !subKeys.length || 640 (!subNames && !subLnames) || 641 (subNames && subKeys.length != subNames.length) || 642 (subLnames && subKeys.length != subLnames.length) 643 ) { 644 return null; 645 } 646 647 // Overwrite subKeys with subIsoids, when available 648 if (subIsoids && subIsoids.length && subIsoids.length == subKeys.length) { 649 for (let i = 0; i < subIsoids.length; i++) { 650 if (subIsoids[i]) { 651 subKeys[i] = subIsoids[i]; 652 } 653 } 654 } 655 656 // Apply sub_lnames if sub_names does not exist 657 let names = subNames || subLnames; 658 return new Map(subKeys.map((key, index) => [key, names[index]])); 659 }, 660 661 /** 662 * Parse a require string and outputs an array of fields. 663 * Spaces, commas, and other literals are ignored in this implementation. 664 * For example, a require string "ACS" should return: 665 * ["street-address", "address-level2", "address-level1"] 666 * 667 * @param {string} requireString Country address require string 668 * @returns {array<string>} List of fields 669 */ 670 parseRequireString(requireString) { 671 if (!requireString) { 672 throw new Error("requireString string is missing."); 673 } 674 675 return requireString.split("").map(fieldId => this.FIELDS_LOOKUP[fieldId]); 676 }, 677 678 /** 679 * Use alternative country name list to identify a country code from a 680 * specified country name. 681 * @param {string} countryName A country name to be identified 682 * @param {string} [countrySpecified] A country code indicating that we only 683 * search its alternative names if specified. 684 * @returns {string} The matching country code. 685 */ 686 identifyCountryCode(countryName, countrySpecified) { 687 let countries = countrySpecified 688 ? [countrySpecified] 689 : [...FormAutofill.countries.keys()]; 690 691 for (let country of countries) { 692 let collators = this.getSearchCollators(country); 693 let metadata = this.getCountryAddressData(country); 694 if (country != metadata.key) { 695 // We hit the fallback logic in getCountryAddressRawData so ignore it as 696 // it's not related to `country` and use the name from l10n instead. 697 metadata = { 698 id: `data/${country}`, 699 key: country, 700 name: FormAutofill.countries.get(country), 701 }; 702 } 703 let alternativeCountryNames = metadata.alternative_names || [ 704 metadata.name, 705 ]; 706 let reAlternativeCountryNames = this._reAlternativeCountryNames[country]; 707 if (!reAlternativeCountryNames) { 708 reAlternativeCountryNames = this._reAlternativeCountryNames[ 709 country 710 ] = []; 711 } 712 713 for (let i = 0; i < alternativeCountryNames.length; i++) { 714 let name = alternativeCountryNames[i]; 715 let reName = reAlternativeCountryNames[i]; 716 if (!reName) { 717 reName = reAlternativeCountryNames[i] = new RegExp( 718 "\\b" + this.escapeRegExp(name) + "\\b", 719 "i" 720 ); 721 } 722 723 if ( 724 this.strCompare(name, countryName, collators) || 725 reName.test(countryName) 726 ) { 727 return country; 728 } 729 } 730 } 731 732 return null; 733 }, 734 735 findSelectOption(selectEl, record, fieldName) { 736 if (this.isAddressField(fieldName)) { 737 return this.findAddressSelectOption(selectEl, record, fieldName); 738 } 739 if (this.isCreditCardField(fieldName)) { 740 return this.findCreditCardSelectOption(selectEl, record, fieldName); 741 } 742 return null; 743 }, 744 745 /** 746 * Try to find the abbreviation of the given sub-region name 747 * @param {string[]} subregionValues A list of inferable sub-region values. 748 * @param {string} [country] A country name to be identified. 749 * @returns {string} The matching sub-region abbreviation. 750 */ 751 getAbbreviatedSubregionName(subregionValues, country) { 752 let values = Array.isArray(subregionValues) 753 ? subregionValues 754 : [subregionValues]; 755 756 let collators = this.getSearchCollators(country); 757 for (let metadata of this.getCountryAddressDataWithLocales(country)) { 758 let { 759 sub_keys: subKeys, 760 sub_names: subNames, 761 sub_lnames: subLnames, 762 } = metadata; 763 if (!subKeys) { 764 // Not all regions have sub_keys. e.g. DE 765 continue; 766 } 767 // Apply sub_lnames if sub_names does not exist 768 subNames = subNames || subLnames; 769 770 let speculatedSubIndexes = []; 771 for (const val of values) { 772 let identifiedValue = this.identifyValue( 773 subKeys, 774 subNames, 775 val, 776 collators 777 ); 778 if (identifiedValue) { 779 return identifiedValue; 780 } 781 782 // Predict the possible state by partial-matching if no exact match. 783 [subKeys, subNames].forEach(sub => { 784 speculatedSubIndexes.push( 785 sub.findIndex(token => { 786 let pattern = new RegExp( 787 "\\b" + this.escapeRegExp(token) + "\\b" 788 ); 789 790 return pattern.test(val); 791 }) 792 ); 793 }); 794 } 795 let subKey = subKeys[speculatedSubIndexes.find(i => !!~i)]; 796 if (subKey) { 797 return subKey; 798 } 799 } 800 return null; 801 }, 802 803 /** 804 * Find the option element from select element. 805 * 1. Try to find the locale using the country from address. 806 * 2. First pass try to find exact match. 807 * 3. Second pass try to identify values from address value and options, 808 * and look for a match. 809 * @param {DOMElement} selectEl 810 * @param {object} address 811 * @param {string} fieldName 812 * @returns {DOMElement} 813 */ 814 findAddressSelectOption(selectEl, address, fieldName) { 815 if (selectEl.options.length > 512) { 816 // Allow enough space for all countries (roughly 300 distinct values) and all 817 // timezones (roughly 400 distinct values), plus some extra wiggle room. 818 return null; 819 } 820 let value = address[fieldName]; 821 if (!value) { 822 return null; 823 } 824 825 let collators = this.getSearchCollators(address.country); 826 827 for (let option of selectEl.options) { 828 if ( 829 this.strCompare(value, option.value, collators) || 830 this.strCompare(value, option.text, collators) 831 ) { 832 return option; 833 } 834 } 835 836 switch (fieldName) { 837 case "address-level1": { 838 let { country } = address; 839 let identifiedValue = this.getAbbreviatedSubregionName( 840 [value], 841 country 842 ); 843 // No point going any further if we cannot identify value from address level 1 844 if (!identifiedValue) { 845 return null; 846 } 847 for (let dataset of this.getCountryAddressDataWithLocales(country)) { 848 let keys = dataset.sub_keys; 849 if (!keys) { 850 // Not all regions have sub_keys. e.g. DE 851 continue; 852 } 853 // Apply sub_lnames if sub_names does not exist 854 let names = dataset.sub_names || dataset.sub_lnames; 855 856 // Go through options one by one to find a match. 857 // Also check if any option contain the address-level1 key. 858 let pattern = new RegExp( 859 "\\b" + this.escapeRegExp(identifiedValue) + "\\b", 860 "i" 861 ); 862 for (let option of selectEl.options) { 863 let optionValue = this.identifyValue( 864 keys, 865 names, 866 option.value, 867 collators 868 ); 869 let optionText = this.identifyValue( 870 keys, 871 names, 872 option.text, 873 collators 874 ); 875 if ( 876 identifiedValue === optionValue || 877 identifiedValue === optionText || 878 pattern.test(option.value) 879 ) { 880 return option; 881 } 882 } 883 } 884 break; 885 } 886 case "country": { 887 if (this.getCountryAddressData(value).alternative_names) { 888 for (let option of selectEl.options) { 889 if ( 890 this.identifyCountryCode(option.text, value) || 891 this.identifyCountryCode(option.value, value) 892 ) { 893 return option; 894 } 895 } 896 } 897 break; 898 } 899 } 900 901 return null; 902 }, 903 904 findCreditCardSelectOption(selectEl, creditCard, fieldName) { 905 let oneDigitMonth = creditCard["cc-exp-month"] 906 ? creditCard["cc-exp-month"].toString() 907 : null; 908 let twoDigitsMonth = oneDigitMonth ? oneDigitMonth.padStart(2, "0") : null; 909 let fourDigitsYear = creditCard["cc-exp-year"] 910 ? creditCard["cc-exp-year"].toString() 911 : null; 912 let twoDigitsYear = fourDigitsYear ? fourDigitsYear.substr(2, 2) : null; 913 let options = Array.from(selectEl.options); 914 915 switch (fieldName) { 916 case "cc-exp-month": { 917 if (!oneDigitMonth) { 918 return null; 919 } 920 for (let option of options) { 921 if ( 922 [option.text, option.label, option.value].some(s => { 923 let result = /[1-9]\d*/.exec(s); 924 return result && result[0] == oneDigitMonth; 925 }) 926 ) { 927 return option; 928 } 929 } 930 break; 931 } 932 case "cc-exp-year": { 933 if (!fourDigitsYear) { 934 return null; 935 } 936 for (let option of options) { 937 if ( 938 [option.text, option.label, option.value].some( 939 s => s == twoDigitsYear || s == fourDigitsYear 940 ) 941 ) { 942 return option; 943 } 944 } 945 break; 946 } 947 case "cc-exp": { 948 if (!oneDigitMonth || !fourDigitsYear) { 949 return null; 950 } 951 let patterns = [ 952 oneDigitMonth + "/" + twoDigitsYear, // 8/22 953 oneDigitMonth + "/" + fourDigitsYear, // 8/2022 954 twoDigitsMonth + "/" + twoDigitsYear, // 08/22 955 twoDigitsMonth + "/" + fourDigitsYear, // 08/2022 956 oneDigitMonth + "-" + twoDigitsYear, // 8-22 957 oneDigitMonth + "-" + fourDigitsYear, // 8-2022 958 twoDigitsMonth + "-" + twoDigitsYear, // 08-22 959 twoDigitsMonth + "-" + fourDigitsYear, // 08-2022 960 twoDigitsYear + "-" + twoDigitsMonth, // 22-08 961 fourDigitsYear + "-" + twoDigitsMonth, // 2022-08 962 fourDigitsYear + "/" + oneDigitMonth, // 2022/8 963 twoDigitsMonth + twoDigitsYear, // 0822 964 twoDigitsYear + twoDigitsMonth, // 2208 965 ]; 966 967 for (let option of options) { 968 if ( 969 [option.text, option.label, option.value].some(str => 970 patterns.some(pattern => str.includes(pattern)) 971 ) 972 ) { 973 return option; 974 } 975 } 976 break; 977 } 978 case "cc-type": { 979 let network = creditCard["cc-type"] || ""; 980 for (let option of options) { 981 if ( 982 [option.text, option.label, option.value].some( 983 s => CreditCard.getNetworkFromName(s) == network 984 ) 985 ) { 986 return option; 987 } 988 } 989 break; 990 } 991 } 992 993 return null; 994 }, 995 996 /** 997 * Try to match value with keys and names, but always return the key. 998 * @param {array<string>} keys 999 * @param {array<string>} names 1000 * @param {string} value 1001 * @param {array} collators 1002 * @returns {string} 1003 */ 1004 identifyValue(keys, names, value, collators) { 1005 let resultKey = keys.find(key => this.strCompare(value, key, collators)); 1006 if (resultKey) { 1007 return resultKey; 1008 } 1009 1010 let index = names.findIndex(name => 1011 this.strCompare(value, name, collators) 1012 ); 1013 if (index !== -1) { 1014 return keys[index]; 1015 } 1016 1017 return null; 1018 }, 1019 1020 /** 1021 * Compare if two strings are the same. 1022 * @param {string} a 1023 * @param {string} b 1024 * @param {array} collators 1025 * @returns {boolean} 1026 */ 1027 strCompare(a = "", b = "", collators) { 1028 return collators.some(collator => !collator.compare(a, b)); 1029 }, 1030 1031 /** 1032 * Escaping user input to be treated as a literal string within a regular 1033 * expression. 1034 * @param {string} string 1035 * @returns {string} 1036 */ 1037 escapeRegExp(string) { 1038 return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); 1039 }, 1040 1041 /** 1042 * Get formatting information of a given country 1043 * @param {string} country 1044 * @returns {object} 1045 * { 1046 * {string} addressLevel3Label 1047 * {string} addressLevel2Label 1048 * {string} addressLevel1Label 1049 * {string} postalCodeLabel 1050 * {object} fieldsOrder 1051 * {string} postalCodePattern 1052 * } 1053 */ 1054 getFormFormat(country) { 1055 let dataset = this.getCountryAddressData(country); 1056 // We hit a country fallback in `getCountryAddressRawData` but it's not relevant here. 1057 if (country != dataset.key) { 1058 // Use a sparse object so the below default values take effect. 1059 dataset = { 1060 /** 1061 * Even though data/ZZ only has address-level2, include the other levels 1062 * in case they are needed for unknown countries. Users can leave the 1063 * unnecessary fields blank which is better than forcing users to enter 1064 * the data in incorrect fields. 1065 */ 1066 fmt: "%N%n%O%n%A%n%C %S %Z", 1067 }; 1068 } 1069 return { 1070 // When particular values are missing for a country, the 1071 // data/ZZ value should be used instead: 1072 // https://chromium-i18n.appspot.com/ssl-aggregate-address/data/ZZ 1073 addressLevel3Label: dataset.sublocality_name_type || "suburb", 1074 addressLevel2Label: dataset.locality_name_type || "city", 1075 addressLevel1Label: dataset.state_name_type || "province", 1076 addressLevel1Options: this.buildRegionMapIfAvailable( 1077 dataset.sub_keys, 1078 dataset.sub_isoids, 1079 dataset.sub_names, 1080 dataset.sub_lnames 1081 ), 1082 countryRequiredFields: this.parseRequireString(dataset.require || "AC"), 1083 fieldsOrder: this.parseAddressFormat(dataset.fmt || "%N%n%O%n%A%n%C"), 1084 postalCodeLabel: dataset.zip_name_type || "postalCode", 1085 postalCodePattern: dataset.zip, 1086 }; 1087 }, 1088 1089 /** 1090 * Localize "data-localization" or "data-localization-region" attributes. 1091 * @param {Element} element 1092 * @param {string} attributeName 1093 */ 1094 localizeAttributeForElement(element, attributeName) { 1095 switch (attributeName) { 1096 case "data-localization": { 1097 element.textContent = this.stringBundle.GetStringFromName( 1098 element.getAttribute(attributeName) 1099 ); 1100 element.removeAttribute(attributeName); 1101 break; 1102 } 1103 case "data-localization-region": { 1104 let regionCode = element.getAttribute(attributeName); 1105 element.textContent = Services.intl.getRegionDisplayNames(undefined, [ 1106 regionCode, 1107 ]); 1108 element.removeAttribute(attributeName); 1109 return; 1110 } 1111 default: 1112 throw new Error("Unexpected attributeName"); 1113 } 1114 }, 1115 1116 /** 1117 * Localize elements with "data-localization" or "data-localization-region" attributes. 1118 * @param {Element} root 1119 */ 1120 localizeMarkup(root) { 1121 let elements = root.querySelectorAll("[data-localization]"); 1122 for (let element of elements) { 1123 this.localizeAttributeForElement(element, "data-localization"); 1124 } 1125 1126 elements = root.querySelectorAll("[data-localization-region]"); 1127 for (let element of elements) { 1128 this.localizeAttributeForElement(element, "data-localization-region"); 1129 } 1130 }, 1131}; 1132 1133this.log = null; 1134FormAutofill.defineLazyLogGetter(this, EXPORTED_SYMBOLS[0]); 1135 1136XPCOMUtils.defineLazyGetter(FormAutofillUtils, "stringBundle", function() { 1137 return Services.strings.createBundle( 1138 "chrome://formautofill/locale/formautofill.properties" 1139 ); 1140}); 1141 1142XPCOMUtils.defineLazyGetter(FormAutofillUtils, "brandBundle", function() { 1143 return Services.strings.createBundle( 1144 "chrome://branding/locale/brand.properties" 1145 ); 1146}); 1147 1148XPCOMUtils.defineLazyPreferenceGetter( 1149 FormAutofillUtils, 1150 "_reauthEnabledByUser", 1151 "extensions.formautofill.reauth.enabled", 1152 false 1153); 1154