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 ALLOWED_TYPES: ["text", "email", "tel", "number", "month"], 441 isFieldEligibleForAutofill(element) { 442 let tagName = element.tagName; 443 if (tagName == "INPUT") { 444 // `element.type` can be recognized as `text`, if it's missing or invalid. 445 if (!this.ALLOWED_TYPES.includes(element.type)) { 446 return false; 447 } 448 } else if (tagName != "SELECT") { 449 return false; 450 } 451 452 return true; 453 }, 454 455 loadDataFromScript(url, sandbox = {}) { 456 Services.scriptloader.loadSubScript(url, sandbox); 457 return sandbox; 458 }, 459 460 /** 461 * Get country address data and fallback to US if not found. 462 * See AddressDataLoader._loadData for more details of addressData structure. 463 * @param {string} [country=FormAutofill.DEFAULT_REGION] 464 * The country code for requesting specific country's metadata. It'll be 465 * default region if parameter is not set. 466 * @param {string} [level1=null] 467 * Return address level 1/level 2 metadata if parameter is set. 468 * @returns {object|null} 469 * Return metadata of specific region with default locale and other supported 470 * locales. We need to return a default country metadata for layout format 471 * and collator, but for sub-region metadata we'll just return null if not found. 472 */ 473 getCountryAddressRawData( 474 country = FormAutofill.DEFAULT_REGION, 475 level1 = null 476 ) { 477 let metadata = AddressDataLoader.getData(country, level1); 478 if (!metadata) { 479 if (level1) { 480 return null; 481 } 482 // Fallback to default region if we couldn't get data from given country. 483 if (country != FormAutofill.DEFAULT_REGION) { 484 metadata = AddressDataLoader.getData(FormAutofill.DEFAULT_REGION); 485 } 486 } 487 488 // TODO: Now we fallback to US if we couldn't get data from default region, 489 // but it could be removed in bug 1423464 if it's not necessary. 490 if (!metadata) { 491 metadata = AddressDataLoader.getData("US"); 492 } 493 return metadata; 494 }, 495 496 /** 497 * Get country address data with default locale. 498 * @param {string} country 499 * @param {string} level1 500 * @returns {object|null} Return metadata of specific region with default locale. 501 * NOTE: The returned data may be for a default region if the 502 * specified one cannot be found. Callers who only want the specific 503 * region should check the returned country code. 504 */ 505 getCountryAddressData(country, level1) { 506 let metadata = this.getCountryAddressRawData(country, level1); 507 return metadata && metadata.defaultLocale; 508 }, 509 510 /** 511 * Get country address data with all locales. 512 * @param {string} country 513 * @param {string} level1 514 * @returns {array<object>|null} 515 * Return metadata of specific region with all the locales. 516 * NOTE: The returned data may be for a default region if the 517 * specified one cannot be found. Callers who only want the specific 518 * region should check the returned country code. 519 */ 520 getCountryAddressDataWithLocales(country, level1) { 521 let metadata = this.getCountryAddressRawData(country, level1); 522 return metadata && [metadata.defaultLocale, ...metadata.locales]; 523 }, 524 525 /** 526 * Get the collators based on the specified country. 527 * @param {string} country The specified country. 528 * @returns {array} An array containing several collator objects. 529 */ 530 getSearchCollators(country) { 531 // TODO: Only one language should be used at a time per country. The locale 532 // of the page should be taken into account to do this properly. 533 // We are going to support more countries in bug 1370193 and this 534 // should be addressed when we start to implement that bug. 535 536 if (!this._collators[country]) { 537 let dataset = this.getCountryAddressData(country); 538 let languages = dataset.languages || [dataset.lang]; 539 let options = { 540 ignorePunctuation: true, 541 sensitivity: "base", 542 usage: "search", 543 }; 544 this._collators[country] = languages.map( 545 lang => new Intl.Collator(lang, options) 546 ); 547 } 548 return this._collators[country]; 549 }, 550 551 // Based on the list of fields abbreviations in 552 // https://github.com/googlei18n/libaddressinput/wiki/AddressValidationMetadata 553 FIELDS_LOOKUP: { 554 N: "name", 555 O: "organization", 556 A: "street-address", 557 S: "address-level1", 558 C: "address-level2", 559 D: "address-level3", 560 Z: "postal-code", 561 n: "newLine", 562 }, 563 564 /** 565 * Parse a country address format string and outputs an array of fields. 566 * Spaces, commas, and other literals are ignored in this implementation. 567 * For example, format string "%A%n%C, %S" should return: 568 * [ 569 * {fieldId: "street-address", newLine: true}, 570 * {fieldId: "address-level2"}, 571 * {fieldId: "address-level1"}, 572 * ] 573 * 574 * @param {string} fmt Country address format string 575 * @returns {array<object>} List of fields 576 */ 577 parseAddressFormat(fmt) { 578 if (!fmt) { 579 throw new Error("fmt string is missing."); 580 } 581 582 return fmt.match(/%[^%]/g).reduce((parsed, part) => { 583 // Take the first letter of each segment and try to identify it 584 let fieldId = this.FIELDS_LOOKUP[part[1]]; 585 // Early return if cannot identify part. 586 if (!fieldId) { 587 return parsed; 588 } 589 // If a new line is detected, add an attribute to the previous field. 590 if (fieldId == "newLine") { 591 let size = parsed.length; 592 if (size) { 593 parsed[size - 1].newLine = true; 594 } 595 return parsed; 596 } 597 return parsed.concat({ fieldId }); 598 }, []); 599 }, 600 601 /** 602 * Used to populate dropdowns in the UI (e.g. FormAutofill preferences, Web Payments). 603 * Use findAddressSelectOption for matching a value to a region. 604 * 605 * @param {string[]} subKeys An array of regionCode strings 606 * @param {string[]} subIsoids An array of ISO ID strings, if provided will be preferred over the key 607 * @param {string[]} subNames An array of regionName strings 608 * @param {string[]} subLnames An array of latinised regionName strings 609 * @returns {Map?} Returns null if subKeys or subNames are not truthy. 610 * Otherwise, a Map will be returned mapping keys -> names. 611 */ 612 buildRegionMapIfAvailable(subKeys, subIsoids, subNames, subLnames) { 613 // Not all regions have sub_keys. e.g. DE 614 if ( 615 !subKeys || 616 !subKeys.length || 617 (!subNames && !subLnames) || 618 (subNames && subKeys.length != subNames.length) || 619 (subLnames && subKeys.length != subLnames.length) 620 ) { 621 return null; 622 } 623 624 // Overwrite subKeys with subIsoids, when available 625 if (subIsoids && subIsoids.length && subIsoids.length == subKeys.length) { 626 for (let i = 0; i < subIsoids.length; i++) { 627 if (subIsoids[i]) { 628 subKeys[i] = subIsoids[i]; 629 } 630 } 631 } 632 633 // Apply sub_lnames if sub_names does not exist 634 let names = subNames || subLnames; 635 return new Map(subKeys.map((key, index) => [key, names[index]])); 636 }, 637 638 /** 639 * Parse a require string and outputs an array of fields. 640 * Spaces, commas, and other literals are ignored in this implementation. 641 * For example, a require string "ACS" should return: 642 * ["street-address", "address-level2", "address-level1"] 643 * 644 * @param {string} requireString Country address require string 645 * @returns {array<string>} List of fields 646 */ 647 parseRequireString(requireString) { 648 if (!requireString) { 649 throw new Error("requireString string is missing."); 650 } 651 652 return requireString.split("").map(fieldId => this.FIELDS_LOOKUP[fieldId]); 653 }, 654 655 /** 656 * Use alternative country name list to identify a country code from a 657 * specified country name. 658 * @param {string} countryName A country name to be identified 659 * @param {string} [countrySpecified] A country code indicating that we only 660 * search its alternative names if specified. 661 * @returns {string} The matching country code. 662 */ 663 identifyCountryCode(countryName, countrySpecified) { 664 let countries = countrySpecified 665 ? [countrySpecified] 666 : [...FormAutofill.countries.keys()]; 667 668 for (let country of countries) { 669 let collators = this.getSearchCollators(country); 670 let metadata = this.getCountryAddressData(country); 671 if (country != metadata.key) { 672 // We hit the fallback logic in getCountryAddressRawData so ignore it as 673 // it's not related to `country` and use the name from l10n instead. 674 metadata = { 675 id: `data/${country}`, 676 key: country, 677 name: FormAutofill.countries.get(country), 678 }; 679 } 680 let alternativeCountryNames = metadata.alternative_names || [ 681 metadata.name, 682 ]; 683 let reAlternativeCountryNames = this._reAlternativeCountryNames[country]; 684 if (!reAlternativeCountryNames) { 685 reAlternativeCountryNames = this._reAlternativeCountryNames[ 686 country 687 ] = []; 688 } 689 690 for (let i = 0; i < alternativeCountryNames.length; i++) { 691 let name = alternativeCountryNames[i]; 692 let reName = reAlternativeCountryNames[i]; 693 if (!reName) { 694 reName = reAlternativeCountryNames[i] = new RegExp( 695 "\\b" + this.escapeRegExp(name) + "\\b", 696 "i" 697 ); 698 } 699 700 if ( 701 this.strCompare(name, countryName, collators) || 702 reName.test(countryName) 703 ) { 704 return country; 705 } 706 } 707 } 708 709 return null; 710 }, 711 712 findSelectOption(selectEl, record, fieldName) { 713 if (this.isAddressField(fieldName)) { 714 return this.findAddressSelectOption(selectEl, record, fieldName); 715 } 716 if (this.isCreditCardField(fieldName)) { 717 return this.findCreditCardSelectOption(selectEl, record, fieldName); 718 } 719 return null; 720 }, 721 722 /** 723 * Try to find the abbreviation of the given sub-region name 724 * @param {string[]} subregionValues A list of inferable sub-region values. 725 * @param {string} [country] A country name to be identified. 726 * @returns {string} The matching sub-region abbreviation. 727 */ 728 getAbbreviatedSubregionName(subregionValues, country) { 729 let values = Array.isArray(subregionValues) 730 ? subregionValues 731 : [subregionValues]; 732 733 let collators = this.getSearchCollators(country); 734 for (let metadata of this.getCountryAddressDataWithLocales(country)) { 735 let { 736 sub_keys: subKeys, 737 sub_names: subNames, 738 sub_lnames: subLnames, 739 } = metadata; 740 if (!subKeys) { 741 // Not all regions have sub_keys. e.g. DE 742 continue; 743 } 744 // Apply sub_lnames if sub_names does not exist 745 subNames = subNames || subLnames; 746 747 let speculatedSubIndexes = []; 748 for (const val of values) { 749 let identifiedValue = this.identifyValue( 750 subKeys, 751 subNames, 752 val, 753 collators 754 ); 755 if (identifiedValue) { 756 return identifiedValue; 757 } 758 759 // Predict the possible state by partial-matching if no exact match. 760 [subKeys, subNames].forEach(sub => { 761 speculatedSubIndexes.push( 762 sub.findIndex(token => { 763 let pattern = new RegExp( 764 "\\b" + this.escapeRegExp(token) + "\\b" 765 ); 766 767 return pattern.test(val); 768 }) 769 ); 770 }); 771 } 772 let subKey = subKeys[speculatedSubIndexes.find(i => !!~i)]; 773 if (subKey) { 774 return subKey; 775 } 776 } 777 return null; 778 }, 779 780 /** 781 * Find the option element from select element. 782 * 1. Try to find the locale using the country from address. 783 * 2. First pass try to find exact match. 784 * 3. Second pass try to identify values from address value and options, 785 * and look for a match. 786 * @param {DOMElement} selectEl 787 * @param {object} address 788 * @param {string} fieldName 789 * @returns {DOMElement} 790 */ 791 findAddressSelectOption(selectEl, address, fieldName) { 792 let value = address[fieldName]; 793 if (!value) { 794 return null; 795 } 796 797 let collators = this.getSearchCollators(address.country); 798 799 for (let option of selectEl.options) { 800 if ( 801 this.strCompare(value, option.value, collators) || 802 this.strCompare(value, option.text, collators) 803 ) { 804 return option; 805 } 806 } 807 808 switch (fieldName) { 809 case "address-level1": { 810 let { country } = address; 811 let identifiedValue = this.getAbbreviatedSubregionName( 812 [value], 813 country 814 ); 815 // No point going any further if we cannot identify value from address level 1 816 if (!identifiedValue) { 817 return null; 818 } 819 for (let dataset of this.getCountryAddressDataWithLocales(country)) { 820 let keys = dataset.sub_keys; 821 if (!keys) { 822 // Not all regions have sub_keys. e.g. DE 823 continue; 824 } 825 // Apply sub_lnames if sub_names does not exist 826 let names = dataset.sub_names || dataset.sub_lnames; 827 828 // Go through options one by one to find a match. 829 // Also check if any option contain the address-level1 key. 830 let pattern = new RegExp( 831 "\\b" + this.escapeRegExp(identifiedValue) + "\\b", 832 "i" 833 ); 834 for (let option of selectEl.options) { 835 let optionValue = this.identifyValue( 836 keys, 837 names, 838 option.value, 839 collators 840 ); 841 let optionText = this.identifyValue( 842 keys, 843 names, 844 option.text, 845 collators 846 ); 847 if ( 848 identifiedValue === optionValue || 849 identifiedValue === optionText || 850 pattern.test(option.value) 851 ) { 852 return option; 853 } 854 } 855 } 856 break; 857 } 858 case "country": { 859 if (this.getCountryAddressData(value).alternative_names) { 860 for (let option of selectEl.options) { 861 if ( 862 this.identifyCountryCode(option.text, value) || 863 this.identifyCountryCode(option.value, value) 864 ) { 865 return option; 866 } 867 } 868 } 869 break; 870 } 871 } 872 873 return null; 874 }, 875 876 findCreditCardSelectOption(selectEl, creditCard, fieldName) { 877 let oneDigitMonth = creditCard["cc-exp-month"] 878 ? creditCard["cc-exp-month"].toString() 879 : null; 880 let twoDigitsMonth = oneDigitMonth ? oneDigitMonth.padStart(2, "0") : null; 881 let fourDigitsYear = creditCard["cc-exp-year"] 882 ? creditCard["cc-exp-year"].toString() 883 : null; 884 let twoDigitsYear = fourDigitsYear ? fourDigitsYear.substr(2, 2) : null; 885 let options = Array.from(selectEl.options); 886 887 switch (fieldName) { 888 case "cc-exp-month": { 889 if (!oneDigitMonth) { 890 return null; 891 } 892 for (let option of options) { 893 if ( 894 [option.text, option.label, option.value].some(s => { 895 let result = /[1-9]\d*/.exec(s); 896 return result && result[0] == oneDigitMonth; 897 }) 898 ) { 899 return option; 900 } 901 } 902 break; 903 } 904 case "cc-exp-year": { 905 if (!fourDigitsYear) { 906 return null; 907 } 908 for (let option of options) { 909 if ( 910 [option.text, option.label, option.value].some( 911 s => s == twoDigitsYear || s == fourDigitsYear 912 ) 913 ) { 914 return option; 915 } 916 } 917 break; 918 } 919 case "cc-exp": { 920 if (!oneDigitMonth || !fourDigitsYear) { 921 return null; 922 } 923 let patterns = [ 924 oneDigitMonth + "/" + twoDigitsYear, // 8/22 925 oneDigitMonth + "/" + fourDigitsYear, // 8/2022 926 twoDigitsMonth + "/" + twoDigitsYear, // 08/22 927 twoDigitsMonth + "/" + fourDigitsYear, // 08/2022 928 oneDigitMonth + "-" + twoDigitsYear, // 8-22 929 oneDigitMonth + "-" + fourDigitsYear, // 8-2022 930 twoDigitsMonth + "-" + twoDigitsYear, // 08-22 931 twoDigitsMonth + "-" + fourDigitsYear, // 08-2022 932 twoDigitsYear + "-" + twoDigitsMonth, // 22-08 933 fourDigitsYear + "-" + twoDigitsMonth, // 2022-08 934 fourDigitsYear + "/" + oneDigitMonth, // 2022/8 935 twoDigitsMonth + twoDigitsYear, // 0822 936 twoDigitsYear + twoDigitsMonth, // 2208 937 ]; 938 939 for (let option of options) { 940 if ( 941 [option.text, option.label, option.value].some(str => 942 patterns.some(pattern => str.includes(pattern)) 943 ) 944 ) { 945 return option; 946 } 947 } 948 break; 949 } 950 case "cc-type": { 951 let network = creditCard["cc-type"] || ""; 952 for (let option of options) { 953 if ( 954 [option.text, option.label, option.value].some( 955 s => s.trim().toLowerCase() == network 956 ) 957 ) { 958 return option; 959 } 960 } 961 break; 962 } 963 } 964 965 return null; 966 }, 967 968 /** 969 * Try to match value with keys and names, but always return the key. 970 * @param {array<string>} keys 971 * @param {array<string>} names 972 * @param {string} value 973 * @param {array} collators 974 * @returns {string} 975 */ 976 identifyValue(keys, names, value, collators) { 977 let resultKey = keys.find(key => this.strCompare(value, key, collators)); 978 if (resultKey) { 979 return resultKey; 980 } 981 982 let index = names.findIndex(name => 983 this.strCompare(value, name, collators) 984 ); 985 if (index !== -1) { 986 return keys[index]; 987 } 988 989 return null; 990 }, 991 992 /** 993 * Compare if two strings are the same. 994 * @param {string} a 995 * @param {string} b 996 * @param {array} collators 997 * @returns {boolean} 998 */ 999 strCompare(a = "", b = "", collators) { 1000 return collators.some(collator => !collator.compare(a, b)); 1001 }, 1002 1003 /** 1004 * Escaping user input to be treated as a literal string within a regular 1005 * expression. 1006 * @param {string} string 1007 * @returns {string} 1008 */ 1009 escapeRegExp(string) { 1010 return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); 1011 }, 1012 1013 /** 1014 * Get formatting information of a given country 1015 * @param {string} country 1016 * @returns {object} 1017 * { 1018 * {string} addressLevel3Label 1019 * {string} addressLevel2Label 1020 * {string} addressLevel1Label 1021 * {string} postalCodeLabel 1022 * {object} fieldsOrder 1023 * {string} postalCodePattern 1024 * } 1025 */ 1026 getFormFormat(country) { 1027 let dataset = this.getCountryAddressData(country); 1028 // We hit a country fallback in `getCountryAddressRawData` but it's not relevant here. 1029 if (country != dataset.key) { 1030 // Use a sparse object so the below default values take effect. 1031 dataset = { 1032 /** 1033 * Even though data/ZZ only has address-level2, include the other levels 1034 * in case they are needed for unknown countries. Users can leave the 1035 * unnecessary fields blank which is better than forcing users to enter 1036 * the data in incorrect fields. 1037 */ 1038 fmt: "%N%n%O%n%A%n%C %S %Z", 1039 }; 1040 } 1041 return { 1042 // When particular values are missing for a country, the 1043 // data/ZZ value should be used instead: 1044 // https://chromium-i18n.appspot.com/ssl-aggregate-address/data/ZZ 1045 addressLevel3Label: dataset.sublocality_name_type || "suburb", 1046 addressLevel2Label: dataset.locality_name_type || "city", 1047 addressLevel1Label: dataset.state_name_type || "province", 1048 addressLevel1Options: this.buildRegionMapIfAvailable( 1049 dataset.sub_keys, 1050 dataset.sub_isoids, 1051 dataset.sub_names, 1052 dataset.sub_lnames 1053 ), 1054 countryRequiredFields: this.parseRequireString(dataset.require || "AC"), 1055 fieldsOrder: this.parseAddressFormat(dataset.fmt || "%N%n%O%n%A%n%C"), 1056 postalCodeLabel: dataset.zip_name_type || "postalCode", 1057 postalCodePattern: dataset.zip, 1058 }; 1059 }, 1060 1061 /** 1062 * Localize "data-localization" or "data-localization-region" attributes. 1063 * @param {Element} element 1064 * @param {string} attributeName 1065 */ 1066 localizeAttributeForElement(element, attributeName) { 1067 switch (attributeName) { 1068 case "data-localization": { 1069 element.textContent = this.stringBundle.GetStringFromName( 1070 element.getAttribute(attributeName) 1071 ); 1072 element.removeAttribute(attributeName); 1073 break; 1074 } 1075 case "data-localization-region": { 1076 let regionCode = element.getAttribute(attributeName); 1077 element.textContent = Services.intl.getRegionDisplayNames(undefined, [ 1078 regionCode, 1079 ]); 1080 element.removeAttribute(attributeName); 1081 return; 1082 } 1083 default: 1084 throw new Error("Unexpected attributeName"); 1085 } 1086 }, 1087 1088 /** 1089 * Localize elements with "data-localization" or "data-localization-region" attributes. 1090 * @param {Element} root 1091 */ 1092 localizeMarkup(root) { 1093 let elements = root.querySelectorAll("[data-localization]"); 1094 for (let element of elements) { 1095 this.localizeAttributeForElement(element, "data-localization"); 1096 } 1097 1098 elements = root.querySelectorAll("[data-localization-region]"); 1099 for (let element of elements) { 1100 this.localizeAttributeForElement(element, "data-localization-region"); 1101 } 1102 }, 1103}; 1104 1105this.log = null; 1106FormAutofill.defineLazyLogGetter(this, EXPORTED_SYMBOLS[0]); 1107 1108XPCOMUtils.defineLazyGetter(FormAutofillUtils, "stringBundle", function() { 1109 return Services.strings.createBundle( 1110 "chrome://formautofill/locale/formautofill.properties" 1111 ); 1112}); 1113 1114XPCOMUtils.defineLazyGetter(FormAutofillUtils, "brandBundle", function() { 1115 return Services.strings.createBundle( 1116 "chrome://branding/locale/brand.properties" 1117 ); 1118}); 1119 1120XPCOMUtils.defineLazyPreferenceGetter( 1121 FormAutofillUtils, 1122 "_reauthEnabledByUser", 1123 "extensions.formautofill.reauth.enabled", 1124 false 1125); 1126