Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
firefox / usr / lib / firefox / browser / features / formautofill@mozilla.org.xpi
Size: Mime:
PK
!<µ)«..ˆchrome.manifestPK
!<Upì줁ãapi.jsPK
!<íëàÝÝ
¤ó-background.jsPK
!<hs›{ªª)¤û.chrome/content/FormAutofillFrameScript.jsPK
!<×f¥¨A¨A#¤ì=chrome/content/autofillEditForms.jsPK
!<5¬‡ó¬¬ ¤Õchrome/content/editAddress.xhtmlPK
!<3“Ri‹‹#¤¿‘chrome/content/editCreditCard.xhtmlPK
!<ߎóÐФ‹¢chrome/content/editDialog.jsPK
!<F«4õ;;¤•µchrome/content/formautofill.cssPK
!<?D›Ó5Ó5¤
Áchrome/content/formautofill.xmlPK
!<¡iet™™"¤÷chrome/content/formfill-anchor.svgPK
!<ÿÙæª!ª!"¤öúchrome/content/heuristicsRegexp.jsPK
!<?Ó<Ñmm$¤àchrome/content/icon-address-save.svgPK
!<0³Zqjj&¤ chrome/content/icon-address-update.svgPK
!<QÒù!¶¶+¤=$chrome/content/icon-credit-card-generic.svgPK
!<ó§rýý#¤<'chrome/content/icon-credit-card.svgPK
!</§²ôô¤z*chrome/content/l10n.jsPK
!<¤(¾:^^$¤¢1chrome/content/manageAddresses.xhtmlPK
!<¿ÔL<ll&¤B8chrome/content/manageCreditCards.xhtmlPK
!<t¬¤ò>chrome/content/manageDialog.cssPK
!<äF—pÿ1ÿ1¤1Kchrome/content/manageDialog.jsPK
!<oO6íâ	â	 ¤l}chrome/content/nameReferences.jsPK
!<ï(O˜+¤Œ‡chrome/content/third-party/cc-logo-amex.pngPK
!<°Ba		.¤ïŒchrome/content/third-party/cc-logo-amex@2x.pngPK
!<+©82ØØ4¤B–chrome/content/third-party/cc-logo-cartebancaire.pngPK
!<ۺ¯''7¤l›chrome/content/third-party/cc-logo-cartebancaire@2x.pngPK
!<øu+-¤è§chrome/content/third-party/cc-logo-diners.svgPK
!<Ȋ+]]/¤P¸chrome/content/third-party/cc-logo-discover.pngPK
!<àvá§	§	2¤ú¼chrome/content/third-party/cc-logo-discover@2x.pngPK
!<7+~«¦¦*¤ñÆchrome/content/third-party/cc-logo-jcb.svgPK
!<Â^AEH
H
1¤ßØchrome/content/third-party/cc-logo-mastercard.svgPK
!<±x“ÎÎ*¤væchrome/content/third-party/cc-logo-mir.svgPK
!<ÀzÄ"ÆÆ/¤Œêchrome/content/third-party/cc-logo-unionpay.svgPK
!<ñö '""+¤Ÿchrome/content/third-party/cc-logo-visa.svgPK
!<Å±±¤
chrome/res/FormAutofill.jsmPK
!<í	þcÇZÇZ"¤ôchrome/res/FormAutofillContent.jsmPK
!<lž~;~;%¤ûwchrome/res/FormAutofillDoorhanger.jsmPK
!<îÌh±l‰l‰"¤¼³chrome/res/FormAutofillHandler.jsmPK
!<7Bö%¤h=chrome/res/FormAutofillHeuristics.jsmPK
!<Ûgl¶$¶$$¤ÀÍchrome/res/FormAutofillNameUtils.jsmPK
!<n<€ҩZ©Z!¤¸òchrome/res/FormAutofillParent.jsmPK
!<ü½<JX(X(&¤ Mchrome/res/FormAutofillPreferences.jsmPK
!<)Ï;TÊÊ"¤<vchrome/res/FormAutofillStorage.jsmPK
!<юP%--¤Fychrome/res/FormAutofillSync.jsmPK
!<%—U'¯w¯w ¤¦chrome/res/FormAutofillUtils.jsmPK
!<ŽZݽz'z'¤Šchrome/res/OSKeyStore.jsmPK
!<±֙ñ1ñ1(¤;Fchrome/res/ProfileAutoCompleteResult.jsmPK
!<	N~~/¤rxchrome/res/addressmetadata/addressReferences.jsPK
!<ÑcZ2¤=chrome/res/addressmetadata/addressReferencesExt.jsPK
!<ܬ«º™™'¤™	chrome/res/autocomplete-item-shared.cssPK
!<ûg“55 ¤wchrome/res/autocomplete-item.cssPK
!<PÉڌ¤êchrome/res/editAddress.cssPK
!<qû²ÅŤA-chrome/res/editCreditCard.cssPK
!<bá²3		 ¤A2chrome/res/editDialog-shared.cssPK
!<7D⤁<chrome/res/editDialog.cssPK
!<¼âü??+¤_=chrome/res/phonenumberutils/PhoneNumber.jsmPK
!<ôg`RÚÚ3¤®|chrome/res/phonenumberutils/PhoneNumberMetaData.jsmPK
!<òY.ýtt5¤ك	chrome/res/phonenumberutils/PhoneNumberNormalizer.jsmPK
!<2W˜Þ99* ‹	en-US/locale/en-US/formautofill.propertiesPK
!<¦ß~‹ªª
¤!ž	manifest.jsonPK
!<DÒhp¤öŸ	schema.jsonPK==nPK
!<µ)«..chrome.manifestlocale formautofill en-US en-US/locale/en-US/
PK
!<Upììapi.js/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

/* globals ExtensionAPI */

const CACHED_STYLESHEETS = new WeakMap();

ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");

ChromeUtils.defineModuleGetter(this, "FormAutofill",
                               "resource://formautofill/FormAutofill.jsm");
ChromeUtils.defineModuleGetter(this, "formAutofillParent",
                               "resource://formautofill/FormAutofillParent.jsm");

XPCOMUtils.defineLazyServiceGetter(this, "resProto",
                                   "@mozilla.org/network/protocol;1?name=resource",
                                   "nsISubstitutingProtocolHandler");

const RESOURCE_HOST = "formautofill";

function insertStyleSheet(domWindow, url) {
  let doc = domWindow.document;
  let styleSheetAttr = `href="${url}" type="text/css"`;
  let styleSheet = doc.createProcessingInstruction("xml-stylesheet", styleSheetAttr);

  doc.insertBefore(styleSheet, doc.documentElement);

  if (CACHED_STYLESHEETS.has(domWindow)) {
    CACHED_STYLESHEETS.get(domWindow).push(styleSheet);
  } else {
    CACHED_STYLESHEETS.set(domWindow, [styleSheet]);
  }
}

function onMaybeOpenPopup(evt) {
  let domWindow = evt.target.ownerGlobal;
  if (CACHED_STYLESHEETS.has(domWindow)) {
    // This window already has autofill stylesheets.
    return;
  }

  insertStyleSheet(domWindow, "chrome://formautofill/content/formautofill.css");
  insertStyleSheet(domWindow, "resource://formautofill/autocomplete-item-shared.css");
  insertStyleSheet(domWindow, "resource://formautofill/autocomplete-item.css");
}

function isAvailable() {
  let availablePref = Services.prefs.getCharPref("extensions.formautofill.available");
  if (availablePref == "on") {
    return true;
  } else if (availablePref == "detect") {
    let locale = Services.locale.requestedLocale;
    let region = Services.prefs.getCharPref("browser.search.region", "");
    let supportedCountries = Services.prefs.getCharPref("extensions.formautofill.supportedCountries")
                                           .split(",");
    if (!Services.prefs.getBoolPref("extensions.formautofill.supportRTL") &&
        Services.locale.isAppLocaleRTL) {
      return false;
    }
    return locale == "en-US" && supportedCountries.includes(region);
  }
  return false;
}

this.formautofill = class extends ExtensionAPI {
  onStartup() {
    // We have to do this before actually determining if we're enabled, since
    // there are scripts inside of the core browser code that depend on the
    // FormAutofill JSMs being registered.
    let uri = Services.io.newURI("chrome/res/", null, this.extension.rootURI);
    resProto.setSubstitution(RESOURCE_HOST, uri);

    let aomStartup = Cc["@mozilla.org/addons/addon-manager-startup;1"]
                                 .getService(Ci.amIAddonManagerStartup);
    const manifestURI = Services.io.newURI("manifest.json", null, this.extension.rootURI);
    this.chromeHandle = aomStartup.registerChrome(manifestURI, [
      ["content", "formautofill", "chrome/content/"],
    ]);

    // Until we move to fluent (bug 1446164), we're stuck with
    // chrome.manifest for handling localization since its what the
    // build system can handle for localized repacks.
    if (this.extension.rootURI instanceof Ci.nsIJARURI) {
      this.autofillManifest = this.extension.rootURI.JARFile
                                  .QueryInterface(Ci.nsIFileURL).file;
    } else if (this.extension.rootURI instanceof Ci.nsIFileURL) {
      this.autofillManifest = this.extension.rootURI.file;
    }

    if (this.autofillManifest) {
      Components.manager.addBootstrappedManifestLocation(this.autofillManifest);
    } else {
      Cu.reportError("Cannot find formautofill chrome.manifest for registring translated strings");
    }

    if (!isAvailable()) {
      Services.prefs.clearUserPref("dom.forms.autocomplete.formautofill");
      // reset the sync related prefs incase the feature was previously available
      // but isn't now.
      Services.prefs.clearUserPref("services.sync.engine.addresses.available");
      Services.prefs.clearUserPref("services.sync.engine.creditcards.available");
      Services.telemetry.scalarSet("formautofill.availability", false);
      return;
    }

    // This pref is used for web contents to detect the autocomplete feature.
    // When it's true, "element.autocomplete" will return tokens we currently
    // support -- otherwise it'll return an empty string.
    Services.prefs.setBoolPref("dom.forms.autocomplete.formautofill", true);
    Services.telemetry.scalarSet("formautofill.availability", true);

    // This pref determines whether the "addresses"/"creditcards" sync engine is
    // available (ie, whether it is shown in any UI etc) - it *does not* determine
    // whether the engine is actually enabled or not.
    Services.prefs.setBoolPref("services.sync.engine.addresses.available", true);
    if (FormAutofill.isAutofillCreditCardsAvailable) {
      Services.prefs.setBoolPref("services.sync.engine.creditcards.available", true);
    } else {
      Services.prefs.clearUserPref("services.sync.engine.creditcards.available");
    }

    // Listen for the autocomplete popup message to lazily append our stylesheet related to the popup.
    Services.mm.addMessageListener("FormAutoComplete:MaybeOpenPopup", onMaybeOpenPopup);

    formAutofillParent.init().catch(Cu.reportError);
    /* eslint-disable no-unused-vars */
    Services.ppmm.loadProcessScript("data:,new " + function() {
      ChromeUtils.import("resource://formautofill/FormAutofillContent.jsm");
    }, true);
    /* eslint-enable no-unused-vars */
    Services.mm.loadFrameScript("chrome://formautofill/content/FormAutofillFrameScript.js", true, true);
  }

  onShutdown() {
    resProto.setSubstitution(RESOURCE_HOST, null);

    this.chromeHandle.destruct();
    this.chromeHandle = null;

    if (this.autofillManifest) {
      Components.manager.removeBootstrappedManifestLocation(this.autofillManifest);
    }

    Services.mm.removeMessageListener("FormAutoComplete:MaybeOpenPopup", onMaybeOpenPopup);

    for (let win of Services.wm.getEnumerator("navigator:browser")) {
      let cachedStyleSheets = CACHED_STYLESHEETS.get(win);

      if (!cachedStyleSheets) {
        continue;
      }

      while (cachedStyleSheets.length !== 0) {
        cachedStyleSheets.pop().remove();
      }
    }
  }
};
PK
!<íëàÝÝ
background.js/* eslint-env webextensions */

"use strict";

browser.runtime.onUpdateAvailable.addListener(details => {
  // By listening to but ignoring this event, any updates will
  // be delayed until the next browser restart.
});
PK
!<hs›{ªª)chrome/content/FormAutofillFrameScript.js/* 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/. */

/*
 * Form Autofill frame script.
 */

"use strict";

/* eslint-env mozilla/frame-script */

ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://formautofill/FormAutofillContent.jsm");
ChromeUtils.defineModuleGetter(this, "setTimeout",
                               "resource://gre/modules/Timer.jsm");
ChromeUtils.defineModuleGetter(this, "FormAutofill",
                               "resource://formautofill/FormAutofill.jsm");
ChromeUtils.defineModuleGetter(this, "FormAutofillUtils",
                               "resource://formautofill/FormAutofillUtils.jsm");

/**
 * Handles content's interactions for the frame.
 *
 * NOTE: Declares it by "var" to make it accessible in unit tests.
 */
var FormAutofillFrameScript = {
  _nextHandleElement: null,
  _alreadyDOMContentLoaded: false,
  _hasDOMContentLoadedHandler: false,
  _hasPendingTask: false,

  _doIdentifyAutofillFields() {
    if (this._hasPendingTask) {
      return;
    }
    this._hasPendingTask = true;

    setTimeout(() => {
      FormAutofillContent.identifyAutofillFields(this._nextHandleElement);
      this._hasPendingTask = false;
      this._nextHandleElement = null;
      // This is for testing purpose only which sends a message to indicate that the
      // form has been identified, and ready to open popup.
      sendAsyncMessage("FormAutofill:FieldsIdentified");
      FormAutofillContent.updateActiveInput();
    });
  },

  init() {
    addEventListener("focusin", this);
    addMessageListener("FormAutofill:PreviewProfile", this);
    addMessageListener("FormAutofill:ClearForm", this);
    addMessageListener("FormAutoComplete:PopupClosed", this);
    addMessageListener("FormAutoComplete:PopupOpened", this);
  },

  handleEvent(evt) {
    if (!evt.isTrusted || !FormAutofill.isAutofillEnabled) {
      return;
    }
    FormAutofillContent.updateActiveInput();

    let element = evt.target;
    if (!FormAutofillUtils.isFieldEligibleForAutofill(element)) {
      return;
    }
    this._nextHandleElement = element;

    if (!this._alreadyDOMContentLoaded) {
      let doc = element.ownerDocument;
      if (doc.readyState === "loading") {
        if (!this._hasDOMContentLoadedHandler) {
          this._hasDOMContentLoadedHandler = true;
          doc.addEventListener("DOMContentLoaded", () => this._doIdentifyAutofillFields(), {once: true});
        }
        return;
      }
      this._alreadyDOMContentLoaded = true;
    }

    this._doIdentifyAutofillFields();
  },

  receiveMessage(message) {
    if (!FormAutofill.isAutofillEnabled) {
      return;
    }

    const doc = content.document;
    const {chromeEventHandler} = doc.ownerGlobal.docShell;

    switch (message.name) {
      case "FormAutofill:PreviewProfile": {
        FormAutofillContent.previewProfile(doc);
        break;
      }
      case "FormAutofill:ClearForm": {
        FormAutofillContent.clearForm();
        break;
      }
      case "FormAutoComplete:PopupClosed": {
        FormAutofillContent.onPopupClosed(message.data.selectedRowStyle);
        Services.tm.dispatchToMainThread(() => {
          chromeEventHandler.removeEventListener("keydown", FormAutofillContent._onKeyDown,
                                                 true);
        });

        break;
      }
      case "FormAutoComplete:PopupOpened": {
        chromeEventHandler.addEventListener("keydown", FormAutofillContent._onKeyDown,
                                            true);
        break;
      }
    }
  },
};

FormAutofillFrameScript.init();
PK
!<×f¥¨A¨A#chrome/content/autofillEditForms.js/* 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 EditAddress, EditCreditCard */
/* eslint-disable mozilla/balanced-listeners */ // Not relevant since the document gets unloaded.

"use strict";

class EditAutofillForm {
  constructor(elements) {
    this._elements = elements;
  }

  /**
   * Fill the form with a record object.
   * @param  {object} [record = {}]
   */
  loadRecord(record = {}) {
    for (let field of this._elements.form.elements) {
      let value = record[field.id];
      value = typeof(value) == "undefined" ? "" : value;

      if (record.guid) {
        field.value = value;
      } else if (field.localName == "select") {
        this.setDefaultSelectedOptionByValue(field, value);
      } else {
        // Use .defaultValue instead of .value to avoid setting the `dirty` flag
        // which triggers form validation UI.
        field.defaultValue = value;
      }
    }
    if (!record.guid) {
      // Reset the dirty value flag and validity state.
      this._elements.form.reset();
    }

    for (let field of this._elements.form.elements) {
      this.updatePopulatedState(field);
    }
  }

  setDefaultSelectedOptionByValue(select, value) {
    for (let option of select.options) {
      option.defaultSelected = option.value == value;
    }
  }

  /**
   * Get a record from the form suitable for a save/update in storage.
   * @returns {object}
   */
  buildFormObject() {
    let initialObject = {};
    if (this.hasMailingAddressFields) {
      // Start with an empty string for each mailing-address field so that any
      // fields hidden for the current country are blanked in the return value.
      initialObject = {
        "street-address": "",
        "address-level3": "",
        "address-level2": "",
        "address-level1": "",
        "postal-code": "",
      };
    }

    return Array.from(this._elements.form.elements).reduce((obj, input) => {
      if (!input.disabled) {
        obj[input.id] = input.value;
      }
      return obj;
    }, initialObject);
  }

  /**
   * Handle events
   *
   * @param  {DOMEvent} event
   */
  handleEvent(event) {
    switch (event.type) {
      case "change": {
        this.handleChange(event);
        break;
      }
      case "input": {
        this.handleInput(event);
        break;
      }
    }
  }

  /**
   * Handle change events
   *
   * @param  {DOMEvent} event
   */
  handleChange(event) {
    this.updatePopulatedState(event.target);
  }

  /**
   * Handle input events
   *
   * @param  {DOMEvent} event
   */
  handleInput(event) {}

  /**
   * Attach event listener
   */
  attachEventListeners() {
    this._elements.form.addEventListener("input", this);
  }

  /**
   * Set the field-populated attribute if the field has a value.
   *
   * @param {DOMElement} field The field that will be checked for a value.
   */
  updatePopulatedState(field) {
    let span = field.parentNode.querySelector(".label-text");
    if (!span) {
      return;
    }
    span.toggleAttribute("field-populated", !!field.value.trim());
  }
}

class EditAddress extends EditAutofillForm {
  /**
   * @param {HTMLElement[]} elements
   * @param {object} record
   * @param {object} config
   * @param {string[]} config.DEFAULT_REGION
   * @param {function} config.getFormFormat Function to return form layout info for a given country.
   * @param {string[]} config.countries
   * @param {boolean} [config.noValidate=undefined] Whether to validate the form
   */
  constructor(elements, record, config) {
    super(elements);

    Object.assign(this, config);
    let {form} = this._elements;
    Object.assign(this._elements, {
      addressLevel3Label: form.querySelector("#address-level3-container > .label-text"),
      addressLevel2Label: form.querySelector("#address-level2-container > .label-text"),
      addressLevel1Label: form.querySelector("#address-level1-container > .label-text"),
      postalCodeLabel: form.querySelector("#postal-code-container > .label-text"),
      country: form.querySelector("#country"),
    });

    this.populateCountries();
    // Need to populate the countries before trying to set the initial country.
    // Also need to use this._record so it has the default country selected.
    this.loadRecord(record);
    this.attachEventListeners();

    form.noValidate = !!config.noValidate;
  }

  loadRecord(record) {
    this._record = record;
    if (!record) {
      record = {
        country: this.DEFAULT_REGION,
      };
    }
    super.loadRecord(record);
    this.formatForm(record.country);
  }

  get hasMailingAddressFields() {
    let {addressFields} = this._elements.form.dataset;
    return !addressFields || addressFields.trim().split(/\s+/).includes("mailing-address");
  }

  /**
   * `mailing-address` is a special attribute token to indicate mailing fields + country.
   *
   * @param {object[]} mailingFieldsOrder - `fieldsOrder` from `getFormFormat`
   * @param {string} addressFields - white-space-separated string of requested address fields to show
   * @returns {object[]} in the same structure as `mailingFieldsOrder` but including non-mail fields
   */
  static computeVisibleFields(mailingFieldsOrder, addressFields) {
    if (addressFields) {
      let requestedFieldClasses = addressFields.trim().split(/\s+/);
      let fieldClasses = [];
      if (requestedFieldClasses.includes("mailing-address")) {
        fieldClasses = fieldClasses.concat(mailingFieldsOrder);
        // `country` isn't part of the `mailingFieldsOrder` so add it when filling a mailing-address
        requestedFieldClasses.splice(requestedFieldClasses.indexOf("mailing-address"), 1,
                                     "country");
      }

      for (let fieldClassName of requestedFieldClasses) {
        fieldClasses.push({
          fieldId: fieldClassName,
          newLine: fieldClassName == "name",
        });
      }
      return fieldClasses;
    }

    // This is the default which is shown in the management interface and includes all fields.
    return mailingFieldsOrder.concat([
      {
        fieldId: "country",
      },
      {
        fieldId: "tel",
      },
      {
        fieldId: "email",
        newLine: true,
      },
    ]);
  }

  /**
   * Format the form based on country. The address-level1 and postal-code labels
   * should be specific to the given country.
   * @param  {string} country
   */
  formatForm(country) {
    const {
      addressLevel3Label,
      addressLevel2Label,
      addressLevel1Label,
      postalCodeLabel,
      fieldsOrder: mailingFieldsOrder,
      postalCodePattern,
      countryRequiredFields,
    } = this.getFormFormat(country);
    this._elements.addressLevel3Label.dataset.localization = addressLevel3Label;
    this._elements.addressLevel2Label.dataset.localization = addressLevel2Label;
    this._elements.addressLevel1Label.dataset.localization = addressLevel1Label;
    this._elements.postalCodeLabel.dataset.localization = postalCodeLabel;
    let addressFields = this._elements.form.dataset.addressFields;
    let extraRequiredFields = this._elements.form.dataset.extraRequiredFields;
    let fieldClasses = EditAddress.computeVisibleFields(mailingFieldsOrder, addressFields);
    let requiredFields = new Set(countryRequiredFields);
    if (extraRequiredFields) {
      for (let extraRequiredField of extraRequiredFields.trim().split(/\s+/)) {
        requiredFields.add(extraRequiredField);
      }
    }
    this.arrangeFields(fieldClasses, requiredFields);
    this.updatePostalCodeValidation(postalCodePattern);
  }

  /**
   * Update address field visibility and order based on libaddressinput data.
   *
   * @param {object[]} fieldsOrder array of objects with `fieldId` and optional `newLine` properties
   * @param {Set} requiredFields Set of `fieldId` strings that mark which fields are required
   */
  arrangeFields(fieldsOrder, requiredFields) {
    /**
     * @see FormAutofillStorage.VALID_ADDRESS_FIELDS
     */
    let fields = [
      // `name` is a wrapper for the 3 name fields.
      "name",
      "organization",
      "street-address",
      "address-level3",
      "address-level2",
      "address-level1",
      "postal-code",
      "country",
      "tel",
      "email",
    ];
    let inputs = [];
    for (let i = 0; i < fieldsOrder.length; i++) {
      let {fieldId, newLine} = fieldsOrder[i];

      let container = this._elements.form.querySelector(`#${fieldId}-container`);
      let containerInputs = [...container.querySelectorAll("input, textarea, select")];
      containerInputs.forEach(function(input) {
        input.disabled = false;
        // libaddressinput doesn't list 'country' or 'name' as required.
        // The additional-name field should never get marked as required.
        input.required = (fieldId == "country" ||
                          fieldId == "name" ||
                          requiredFields.has(fieldId)) &&
                         input.id != "additional-name";
      });
      inputs.push(...containerInputs);
      container.style.display = "flex";
      container.style.order = i;
      container.style.pageBreakAfter = newLine ? "always" : "auto";
      // Remove the field from the list of fields
      fields.splice(fields.indexOf(fieldId), 1);
    }
    for (let i = 0; i < inputs.length; i++) {
      // Assign tabIndex starting from 1
      inputs[i].tabIndex = i + 1;
    }
    // Hide the remaining fields
    for (let field of fields) {
      let container = this._elements.form.querySelector(`#${field}-container`);
      container.style.display = "none";
      for (let input of [...container.querySelectorAll("input, textarea, select")]) {
        input.disabled = true;
      }
    }
  }

  updatePostalCodeValidation(postalCodePattern) {
    let postalCodeInput = this._elements.form.querySelector("#postal-code");
    if (postalCodePattern && postalCodeInput.style.display != "none") {
      postalCodeInput.setAttribute("pattern", postalCodePattern);
    } else {
      postalCodeInput.removeAttribute("pattern");
    }
  }

  populateCountries() {
    let fragment = document.createDocumentFragment();
    // Sort countries by their visible names.
    let countries = [...this.countries.entries()].sort((e1, e2) => e1[1].localeCompare(e2[1]));
    for (let country of countries) {
      let option = new Option();
      option.value = country[0];
      option.dataset.localizationRegion = country[0].toLowerCase();
      fragment.appendChild(option);
    }
    this._elements.country.appendChild(fragment);
  }

  handleChange(event) {
    if (event.target == this._elements.country) {
      this.formatForm(event.target.value);
    }
    super.handleChange(event);
  }

  attachEventListeners() {
    this._elements.form.addEventListener("change", this);
    super.attachEventListeners();
  }
}

class EditCreditCard extends EditAutofillForm {
  /**
   * @param {HTMLElement[]} elements
   * @param {object} record with a decrypted cc-number
   * @param {object} addresses in an object with guid keys for the billing address picker.
   * @param {object} config
   * @param {function} config.isCCNumber Function to determine if a string is a valid CC number.
   * @param {function} config.getSupportedNetworks Function to get the list of card networks
   */
  constructor(elements, record, addresses, config) {
    super(elements);

    this._addresses = addresses;
    Object.assign(this, config);
    Object.assign(this._elements, {
      ccNumber: this._elements.form.querySelector("#cc-number"),
      invalidCardNumberStringElement: this._elements.form.querySelector("#invalidCardNumberString"),
      month: this._elements.form.querySelector("#cc-exp-month"),
      year: this._elements.form.querySelector("#cc-exp-year"),
      ccType: this._elements.form.querySelector("#cc-type"),
      billingAddress: this._elements.form.querySelector("#billingAddressGUID"),
      billingAddressRow: this._elements.form.querySelector(".billingAddressRow"),
    });

    this.loadRecord(record, addresses);
    this.attachEventListeners();
  }

  loadRecord(record, addresses, preserveFieldValues) {
    // _record must be updated before generateYears and generateBillingAddressOptions are called.
    this._record = record;
    this._addresses = addresses;
    this.generateBillingAddressOptions(preserveFieldValues);
    if (!preserveFieldValues) {
      // Re-populating the networks will reset the selected option.
      this.populateNetworks();
      // Re-generating the months will reset the selected option.
      this.generateMonths();
      // Re-generating the years will reset the selected option.
      this.generateYears();
      super.loadRecord(record);

      // Resetting the form in the super.loadRecord won't clear custom validity
      // state so reset it here. Since the cc-number field is disabled upon editing
      // we don't need to recaclulate its validity here.
      this._elements.ccNumber.setCustomValidity("");
    }
  }

  generateMonths() {
    const count = 12;

    // Clear the list
    this._elements.month.textContent = "";

    // Empty month option
    this._elements.month.appendChild(new Option());

    // Populate month list. Format: "month number - month name"
    let dateFormat = new Intl.DateTimeFormat(navigator.language, {month: "long"}).format;
    for (let i = 0; i < count; i++) {
      let monthNumber = (i + 1).toString();
      let monthName = dateFormat(new Date(1970, i));
      let option = new Option();
      option.value = monthNumber;
      // XXX: Bug 1446164 - Localize this string.
      option.textContent = `${monthNumber.padStart(2, "0")} - ${monthName}`;
      this._elements.month.appendChild(option);
    }
  }

  generateYears() {
    const count = 11;
    const currentYear = new Date().getFullYear();
    const ccExpYear = this._record && this._record["cc-exp-year"];

    // Clear the list
    this._elements.year.textContent = "";

    // Provide an empty year option
    this._elements.year.appendChild(new Option());

    if (ccExpYear && ccExpYear < currentYear) {
      this._elements.year.appendChild(new Option(ccExpYear));
    }

    for (let i = 0; i < count; i++) {
      let year = currentYear + i;
      let option = new Option(year);
      this._elements.year.appendChild(option);
    }

    if (ccExpYear && ccExpYear > currentYear + count) {
      this._elements.year.appendChild(new Option(ccExpYear));
    }
  }

  populateNetworks() {
    // Clear the list
    this._elements.ccType.textContent = "";
    let frag = document.createDocumentFragment();
    // include an empty first option
    frag.appendChild(new Option("", ""));

    let supportedNetworks = this.getSupportedNetworks();
    for (let id of supportedNetworks) {
      let option = new Option();
      option.value = id;
      option.dataset.localization = "cardNetwork." + id;
      frag.appendChild(option);
    }
    this._elements.ccType.appendChild(frag);
  }

  generateBillingAddressOptions(preserveFieldValues) {
    let billingAddressGUID;
    if (preserveFieldValues && this._elements.billingAddress.value) {
      billingAddressGUID = this._elements.billingAddress.value;
    } else if (this._record) {
      billingAddressGUID = this._record.billingAddressGUID;
    }

    this._elements.billingAddress.textContent = "";

    this._elements.billingAddress.appendChild(new Option("", ""));

    let hasAddresses = false;
    for (let [guid, address] of Object.entries(this._addresses)) {
      hasAddresses = true;
      let selected = guid == billingAddressGUID;
      let option = new Option(this.getAddressLabel(address), guid, selected, selected);
      this._elements.billingAddress.appendChild(option);
    }

    this._elements.billingAddressRow.hidden = !hasAddresses;
  }

  attachEventListeners() {
    this._elements.form.addEventListener("change", this);
    super.attachEventListeners();
  }

  handleChange(event) {
    super.handleChange(event);

    if (event.target != this._elements.ccNumber) {
      return;
    }

    let ccNumberField = this._elements.ccNumber;

    // Mark the cc-number field as invalid if the number is empty or invalid.
    if (!this.isCCNumber(ccNumberField.value)) {
      let invalidCardNumberString = this._elements.invalidCardNumberStringElement.textContent;
      ccNumberField.setCustomValidity(invalidCardNumberString || " ");
    }
  }

  handleInput(event) {
    // Clear the error message if cc-number is valid
    if (event.target == this._elements.ccNumber &&
        this.isCCNumber(this._elements.ccNumber.value)) {
      this._elements.ccNumber.setCustomValidity("");
    }
    super.handleInput(event);
  }
}
PK
!<5¬‡ó¬¬ chrome/content/editAddress.xhtml<?xml version="1.0" encoding="UTF-8"?>
<!-- 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/. -->
<!DOCTYPE html [
  <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
  %globalDTD;
]>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <title data-localization="addNewAddressTitle"/>
  <link rel="stylesheet" href="resource://formautofill/editDialog-shared.css"/>
  <link rel="stylesheet" href="resource://formautofill/editAddress.css"/>
  <link rel="stylesheet" href="resource://formautofill/editDialog.css"/>
  <script src="chrome://formautofill/content/l10n.js"></script>
  <script src="chrome://formautofill/content/editDialog.js"></script>
  <script src="chrome://formautofill/content/autofillEditForms.js"></script>
</head>
<body dir="&locale.dir;">
  <form id="form" class="editAddressForm" autocomplete="off">
    <!--
        The <span class="label-text" …/> needs to be after the form field in the same element in
        order to get proper label styling with :focus and :moz-ui-invalid.
      -->
    <div id="name-container" class="container">
      <label id="given-name-container">
        <input id="given-name" type="text" required="required"/>
        <span data-localization="givenName" class="label-text"/>
      </label>
      <label id="additional-name-container">
        <input id="additional-name" type="text"/>
        <span data-localization="additionalName" class="label-text"/>
      </label>
      <label id="family-name-container">
        <input id="family-name" type="text" required="required"/>
        <span data-localization="familyName" class="label-text"/>
      </label>
    </div>
    <label id="organization-container" class="container">
      <input id="organization" type="text"/>
      <span data-localization="organization2" class="label-text"/>
    </label>
    <label id="street-address-container" class="container">
      <textarea id="street-address" rows="3"/>
      <span data-localization="streetAddress" class="label-text"/>
    </label>
    <label id="address-level3-container" class="container">
      <input id="address-level3" type="text"/>
      <span class="label-text"/>
    </label>
    <label id="address-level2-container" class="container">
      <input id="address-level2" type="text"/>
      <span class="label-text"/>
    </label>
    <label id="address-level1-container" class="container">
      <input id="address-level1" type="text"/>
      <span class="label-text"/>
    </label>
    <label id="postal-code-container" class="container">
      <input id="postal-code" type="text"/>
      <span class="label-text"/>
    </label>
    <label id="country-container" class="container">
      <select id="country" required="required">
        <option/>
      </select>
      <span data-localization="country" class="label-text"/>
    </label>
    <label id="tel-container" class="container">
      <input id="tel" type="tel"/>
      <span data-localization="tel" class="label-text"/>
    </label>
    <label id="email-container" class="container">
      <input id="email" type="email" required="required"/>
      <span data-localization="email" class="label-text"/>
    </label>
  </form>
  <div id="controls-container">
    <button id="cancel" data-localization="cancelBtnLabel"/>
    <button id="save" disabled="disabled" data-localization="saveBtnLabel"/>
    <span id="country-warning-message" data-localization="countryWarningMessage2"/>
  </div>
  <script type="application/javascript"><![CDATA[
    "use strict";

    let {
      DEFAULT_REGION,
      countries,
    } = FormAutofill;
    let {
      getFormFormat,
    } = FormAutofillUtils;
    let args = window.arguments || [];
    let {
      record,
      noValidate,
    } = args[0] || {};

    /* import-globals-from autofillEditForms.js */
    var fieldContainer = new EditAddress({
      form: document.getElementById("form"),
    }, record, {
      DEFAULT_REGION,
      getFormFormat: getFormFormat.bind(FormAutofillUtils),
      countries,
      noValidate,
    });

    /* import-globals-from editDialog.js */
    new EditAddressDialog({
      title: document.querySelector("title"),
      fieldContainer,
      controlsContainer: document.getElementById("controls-container"),
      cancel: document.getElementById("cancel"),
      save: document.getElementById("save"),
    }, record);
  ]]></script>
</body>
</html>
PK
!<3“Ri‹‹#chrome/content/editCreditCard.xhtml<?xml version="1.0" encoding="UTF-8"?>
<!-- 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/. -->
<!DOCTYPE html [
  <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
  %globalDTD;
]>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <title data-localization="addNewCreditCardTitle"/>
  <link rel="stylesheet" href="resource://formautofill/editDialog-shared.css"/>
  <link rel="stylesheet" href="resource://formautofill/editCreditCard.css"/>
  <link rel="stylesheet" href="resource://formautofill/editDialog.css"/>
  <script src="chrome://formautofill/content/l10n.js"></script>
  <script src="chrome://formautofill/content/editDialog.js"></script>
  <script src="chrome://formautofill/content/autofillEditForms.js"></script>
</head>
<body dir="&locale.dir;">
  <form id="form" class="editCreditCardForm" autocomplete="off">
    <!--
        The <span class="label-text" …/> needs to be after the form field in the same element in
        order to get proper label styling with :focus and :moz-ui-invalid.
      -->
    <label id="cc-number-container" class="container">
      <span id="invalidCardNumberString" hidden="hidden" data-localization="invalidCardNumber"></span>
      <input id="cc-number" type="text" required="required" minlength="9" pattern="[- 0-9]+"/>
      <span data-localization="cardNumber" class="label-text"/>
    </label>
    <label id="cc-exp-month-container" class="container">
      <select id="cc-exp-month" required="required">
        <option/>
      </select>
      <span data-localization="cardExpiresMonth" class="label-text"/>
    </label>
    <label id="cc-exp-year-container" class="container">
      <select id="cc-exp-year" required="required">
        <option/>
      </select>
      <span data-localization="cardExpiresYear" class="label-text"/>
    </label>
    <label id="cc-name-container" class="container">
      <input id="cc-name" type="text" required="required"/>
      <span data-localization="nameOnCard" class="label-text"/>
    </label>
    <label id="cc-type-container" class="container">
      <select id="cc-type" required="required">
      </select>
      <span data-localization="cardNetwork" class="label-text"/>
    </label>
    <label id="cc-csc-container" class="container" hidden="hidden">
      <!-- The CSC container will get filled in by forms that need a CSC (using csc-input.js) -->
    </label>
    <div id="billingAddressGUID-container" class="billingAddressRow container rich-picker">
      <select id="billingAddressGUID" required="required">
      </select>
      <label for="billingAddressGUID" data-localization="billingAddress" class="label-text"/>
    </div>
  </form>
  <div id="controls-container">
    <button id="cancel" data-localization="cancelBtnLabel"/>
    <button id="save" disabled="disabled" data-localization="saveBtnLabel"/>
  </div>
  <script type="application/javascript"><![CDATA[
    "use strict";

    (async () => {
      let {
        getAddressLabel,
        isCCNumber,
        getCreditCardNetworks,
      } = FormAutofillUtils;
      let args = window.arguments || [];
      let {
        record,
      } = args[0] || {};

      let addresses = {};
      for (let address of await formAutofillStorage.addresses.getAll()) {
        addresses[address.guid] = address;
      }

      /* import-globals-from autofillEditForms.js */
      let fieldContainer = new EditCreditCard({
        form: document.getElementById("form"),
      }, record, addresses,
        {
          getAddressLabel: getAddressLabel.bind(FormAutofillUtils),
          isCCNumber: isCCNumber.bind(FormAutofillUtils),
          getSupportedNetworks: getCreditCardNetworks.bind(FormAutofillUtils),
        });

      /* import-globals-from editDialog.js */
      new EditCreditCardDialog({
        title: document.querySelector("title"),
        fieldContainer,
        controlsContainer: document.getElementById("controls-container"),
        cancel: document.getElementById("cancel"),
        save: document.getElementById("save"),
      }, record);
    })();
  ]]></script>
</body>
</html>
PK
!<ߎóÐÐchrome/content/editDialog.js/* 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 EditAddressDialog, EditCreditCardDialog */
/* eslint-disable mozilla/balanced-listeners */ // Not relevant since the document gets unloaded.

"use strict";

// eslint-disable-next-line no-unused-vars
ChromeUtils.import("resource://formautofill/FormAutofill.jsm");
ChromeUtils.import("resource://formautofill/FormAutofillUtils.jsm");

ChromeUtils.defineModuleGetter(this, "formAutofillStorage",
                               "resource://formautofill/FormAutofillStorage.jsm");

class AutofillEditDialog {
  constructor(subStorageName, elements, record) {
    this._storageInitPromise = formAutofillStorage.initialize();
    this._subStorageName = subStorageName;
    this._elements = elements;
    this._record = record;
    this.localizeDocument();
    window.addEventListener("DOMContentLoaded", this, {once: true});
  }

  async init() {
    this.attachEventListeners();
    // For testing only: signal to tests that the dialog is ready for testing.
    // This is likely no longer needed since retrieving from storage is fully
    // handled in manageDialog.js now.
    window.dispatchEvent(new CustomEvent("FormReady"));
  }

  /**
   * Get storage and ensure it has been initialized.
   * @returns {object}
   */
  async getStorage() {
    await this._storageInitPromise;
    return formAutofillStorage[this._subStorageName];
  }

  /**
   * Asks FormAutofillParent to save or update an record.
   * @param  {object} record
   * @param  {string} guid [optional]
   */
  async saveRecord(record, guid) {
    let storage = await this.getStorage();
    if (guid) {
      await storage.update(guid, record);
    } else {
      await storage.add(record);
    }
  }

  /**
   * Handle events
   *
   * @param  {DOMEvent} event
   */
  handleEvent(event) {
    switch (event.type) {
      case "DOMContentLoaded": {
        this.init();
        break;
      }
      case "click": {
        this.handleClick(event);
        break;
      }
      case "input": {
        this.handleInput(event);
        break;
      }
      case "keypress": {
        this.handleKeyPress(event);
        break;
      }
      case "contextmenu": {
        if (!(event.target instanceof HTMLInputElement) &&
            !(event.target instanceof HTMLTextAreaElement)) {
          event.preventDefault();
        }
        break;
      }
    }
  }

  /**
   * Handle click events
   *
   * @param  {DOMEvent} event
   */
  handleClick(event) {
    if (event.target == this._elements.cancel) {
      window.close();
    }
    if (event.target == this._elements.save) {
      this.handleSubmit();
    }
  }

  /**
   * Handle input events
   *
   * @param  {DOMEvent} event
   */
  handleInput(event) {
    // Toggle disabled attribute on the save button based on
    // whether the form is filled or empty.
    if (Object.keys(this._elements.fieldContainer.buildFormObject()).length == 0) {
      this._elements.save.setAttribute("disabled", true);
    } else {
      this._elements.save.removeAttribute("disabled");
    }
  }

  /**
   * Handle key press events
   *
   * @param  {DOMEvent} event
   */
  handleKeyPress(event) {
    if (event.keyCode == KeyEvent.DOM_VK_ESCAPE) {
      window.close();
    }
  }

  /**
   * Attach event listener
   */
  attachEventListeners() {
    window.addEventListener("keypress", this);
    window.addEventListener("contextmenu", this);
    this._elements.controlsContainer.addEventListener("click", this);
    document.addEventListener("input", this);
  }

  // An interface to be inherited.
  localizeDocument() {}
}

class EditAddressDialog extends AutofillEditDialog {
  constructor(elements, record) {
    super("addresses", elements, record);
  }

  localizeDocument() {
    if (this._record) {
      this._elements.title.dataset.localization = "editAddressTitle";
    }
  }

  async handleSubmit() {
    await this.saveRecord(this._elements.fieldContainer.buildFormObject(), this._record ? this._record.guid : null);
    window.close();
  }
}

class EditCreditCardDialog extends AutofillEditDialog {
  constructor(elements, record) {
    super("creditCards", elements, record);
  }

  localizeDocument() {
    if (this._record) {
      this._elements.title.dataset.localization = "editCreditCardTitle";
    }
  }

  async handleSubmit() {
    let creditCard = this._elements.fieldContainer.buildFormObject();
    if (!this._elements.fieldContainer._elements.form.reportValidity()) {
      return;
    }

    try {
      await this.saveRecord(creditCard, this._record ? this._record.guid : null);
      window.close();
    } catch (ex) {
      Cu.reportError(ex);
    }
  }
}
PK
!<F«4õ;;chrome/content/formautofill.css/* 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/. */

#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-profile"],
#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-footer"],
#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-insecureWarning"],
#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-clear-button"] {
  display: block;
  margin: 0;
  padding: 0;
  height: auto;
  min-height: auto;
}

#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-profile"] {
  -moz-binding: url("chrome://formautofill/content/formautofill.xml#autocomplete-profile-listitem");
}

#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-footer"] {
  -moz-binding: url("chrome://formautofill/content/formautofill.xml#autocomplete-profile-listitem-footer");
}

#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-insecureWarning"] {
  -moz-binding: url("chrome://formautofill/content/formautofill.xml#autocomplete-creditcard-insecure-field");
}

#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-clear-button"] {
  -moz-binding: url("chrome://formautofill/content/formautofill.xml#autocomplete-profile-listitem-clear-button");
}
/* Treat @collpased="true" as display: none similar to how it is for XUL elements.
 * https://developer.mozilla.org/en-US/docs/Web/CSS/visibility#Values */
#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-profile"][collapsed="true"],
#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-footer"][collapsed="true"],
#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-insecureWarning"][collapsed="true"],
#PopupAutoComplete > richlistbox > richlistitem[originaltype="autofill-clear-button"][collapsed="true"] {
  display: none;
}

#PopupAutoComplete[firstresultstyle="autofill-profile"] {
  min-width: 150px !important;
}

#PopupAutoComplete[firstresultstyle="autofill-insecureWarning"] {
  min-width: 200px !important;
}

/* Form Autofill Doorhanger */
#autofill-address-notification > popupnotificationcontent > .desc-message-box,
#autofill-credit-card-notification > popupnotificationcontent > .desc-message-box {
  margin-block-end: 12px;
}
#autofill-credit-card-notification > popupnotificationcontent > .desc-message-box > image {
  margin-inline-start: 6px;
  width: 16px;
  height: 16px;
  list-style-image: url(chrome://formautofill/content/icon-credit-card-generic.svg);
}
#autofill-address-notification > popupnotificationcontent > .desc-message-box > description,
#autofill-credit-card-notification > popupnotificationcontent > .desc-message-box > description {
  font-style: italic;
}
PK
!<?D›Ó5Ó5chrome/content/formautofill.xml<?xml version="1.0"?>
<!-- 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/. -->

<bindings id="formautofillBindings"
          xmlns="http://www.mozilla.org/xbl"
          xmlns:html="http://www.w3.org/1999/xhtml"
          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
          xmlns:xbl="http://www.mozilla.org/xbl">

  <binding id="autocomplete-profile-listitem-base" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
    <implementation implements="nsIDOMXULSelectControlItemElement">
      <constructor>
      </constructor>
      <!-- For form autofill, we want to unify the selection no matter by
      keyboard navigation or mouseover in order not to confuse user which
      profile preview is being shown. This field is set to true to indicate
      that selectedIndex of popup should be changed while mouseover item -->
      <field name="selectedByMouseOver">true</field>

      <property name="_stringBundle">
        <getter><![CDATA[
          /* global Services */
          if (!this.__stringBundle) {
            this.__stringBundle = Services.strings.createBundle("chrome://formautofill/locale/formautofill.properties");
          }
          return this.__stringBundle;
        ]]></getter>
      </property>

      <method name="_cleanup">
        <body>
        <![CDATA[
          this.removeAttribute("formautofillattached");
          if (this._itemBox) {
            this._itemBox.removeAttribute("size");
          }
        ]]>
        </body>
      </method>

      <method name="_onOverflow">
        <body></body>
      </method>

      <method name="_onUnderflow">
        <body></body>
      </method>

      <method name="handleOverUnderflow">
        <body></body>
      </method>

      <method name="_adjustAutofillItemLayout">
        <body>
        <![CDATA[
          let outerBoxRect = this.parentNode.getBoundingClientRect();

          // Make item fit in popup as XUL box could not constrain
          // item's width
          this._itemBox.style.width = outerBoxRect.width + "px";
          // Use two-lines layout when width is smaller than 150px or
          // 185px if an image precedes the label.
          let oneLineMinRequiredWidth = this.getAttribute("ac-image") ? 185 : 150;

          if (outerBoxRect.width <= oneLineMinRequiredWidth) {
            this._itemBox.setAttribute("size", "small");
          } else {
            this._itemBox.removeAttribute("size");
          }
        ]]>
        </body>
      </method>
    </implementation>
  </binding>

  <binding id="autocomplete-profile-listitem" extends="chrome://formautofill/content/formautofill.xml#autocomplete-profile-listitem-base">
    <xbl:content xmlns="http://www.w3.org/1999/xhtml">
      <div anonid="autofill-item-box" class="autofill-item-box" xbl:inherits="ac-image">
        <div class="profile-label-col profile-item-col">
          <span anonid="profile-label-affix" class="profile-label-affix"></span>
          <span anonid="profile-label" class="profile-label"></span>
        </div>
        <div class="profile-comment-col profile-item-col">
          <span anonid="profile-comment" class="profile-comment"></span>
        </div>
      </div>
    </xbl:content>

    <implementation implements="nsIDOMXULSelectControlItemElement">
      <constructor>
        <![CDATA[
          this._itemBox = document.getAnonymousElementByAttribute(
            this, "anonid", "autofill-item-box"
          );
          this._labelAffix = document.getAnonymousElementByAttribute(
            this, "anonid", "profile-label-affix"
          );
          this._label = document.getAnonymousElementByAttribute(
            this, "anonid", "profile-label"
          );
          this._comment = document.getAnonymousElementByAttribute(
            this, "anonid", "profile-comment"
          );

          this._adjustAcItem();
        ]]>
      </constructor>

      <property name="selected" onget="return this.getAttribute('selected') == 'true';">
        <setter><![CDATA[
          /* global Cu */
          if (val) {
            this.setAttribute("selected", "true");
          } else {
            this.removeAttribute("selected");
          }

          let {AutoCompletePopup} = ChromeUtils.import("resource://gre/modules/AutoCompletePopup.jsm", {});

          AutoCompletePopup.sendMessageToBrowser("FormAutofill:PreviewProfile");

          return val;
        ]]></setter>
      </property>

      <method name="_adjustAcItem">
        <body>
        <![CDATA[
          this._adjustAutofillItemLayout();
          this.setAttribute("formautofillattached", "true");
          this._itemBox.style.setProperty("--primary-icon", `url(${this.getAttribute("ac-image")})`);

          let {primaryAffix, primary, secondary} = JSON.parse(this.getAttribute("ac-value"));

          this._labelAffix.textContent = primaryAffix;
          this._label.textContent = primary;
          this._comment.textContent = secondary;
        ]]>
        </body>
      </method>
    </implementation>
  </binding>

  <binding id="autocomplete-profile-listitem-footer" extends="chrome://formautofill/content/formautofill.xml#autocomplete-profile-listitem-base">
    <xbl:content xmlns="http://www.w3.org/1999/xhtml">
      <div anonid="autofill-footer" class="autofill-item-box autofill-footer">
        <div anonid="autofill-warning" class="autofill-footer-row autofill-warning">
        </div>
        <div anonid="autofill-option-button" class="autofill-footer-row autofill-button">
        </div>
      </div>
    </xbl:content>

    <handlers>
      <handler event="click" button="0"><![CDATA[
        if (this._warningTextBox.contains(event.originalTarget)) {
          return;
        }

        window.openPreferences("privacy-form-autofill", {origin: "autofillFooter"});
      ]]></handler>
    </handlers>

    <implementation implements="nsIDOMXULSelectControlItemElement">
      <constructor>
        <![CDATA[
          this._itemBox = document.getAnonymousElementByAttribute(
            this, "anonid", "autofill-footer"
          );
          this._optionButton = document.getAnonymousElementByAttribute(
            this, "anonid", "autofill-option-button"
          );
          this._warningTextBox = document.getAnonymousElementByAttribute(
            this, "anonid", "autofill-warning"
          );

          /**
           * A handler for updating warning message once selectedIndex has been changed.
           *
           * There're three different states of warning message:
           * 1. None of addresses were selected: We show all the categories intersection of fields in the
           *    form and fields in the results.
           * 2. An address was selested: Show the additional categories that will also be filled.
           * 3. An address was selected, but the focused category is the same as the only one category: Only show
           * the exact category that we're going to fill in.
           *
           * @private
           * @param {string[]} data.categories
           *        The categories of all the fields contained in the selected address.
           */
          this._updateWarningNote = ({data} = {}) => {
            let categories = (data && data.categories) ? data.categories : this._allFieldCategories;
            // If the length of categories is 1, that means all the fillable fields are in the same
            // category. We will change the way to inform user according to this flag. When the value
            // is true, we show "Also autofills ...", otherwise, show "Autofills ..." only.
            let hasExtraCategories = categories.length > 1;
            // Show the categories in certain order to conform with the spec.
            let orderedCategoryList = [{id: "address", l10nId: "category.address"},
                                       {id: "name", l10nId: "category.name"},
                                       {id: "organization", l10nId: "category.organization2"},
                                       {id: "tel", l10nId: "category.tel"},
                                       {id: "email", l10nId: "category.email"}];
            let showCategories = hasExtraCategories ?
              orderedCategoryList.filter(category => categories.includes(category.id) && category.id != this._focusedCategory) :
              [orderedCategoryList.find(category => category.id == this._focusedCategory)];

            let separator = this._stringBundle.GetStringFromName("fieldNameSeparator");
            let warningTextTmplKey = hasExtraCategories ? "phishingWarningMessage" : "phishingWarningMessage2";
            let categoriesText = showCategories.map(category => this._stringBundle.GetStringFromName(category.l10nId)).join(separator);

            this._warningTextBox.textContent = this._stringBundle.formatStringFromName(warningTextTmplKey,
              [categoriesText], 1);
            this.parentNode.parentNode.adjustHeight();
          };

          this._adjustAcItem();
        ]]>
      </constructor>

      <method name="_onCollapse">
        <body>
        <![CDATA[
          /* global messageManager */

          if (this.showWarningText) {
            messageManager.removeMessageListener("FormAutofill:UpdateWarningMessage", this._updateWarningNote);
          }

          this._itemBox.removeAttribute("no-warning");
        ]]>
        </body>
      </method>

      <method name="_adjustAcItem">
        <body>
        <![CDATA[
          /* global Cu */
          this._adjustAutofillItemLayout();
          this.setAttribute("formautofillattached", "true");

          let {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm", {});
          // TODO: The "Short" suffix is pointless now as normal version string is no longer needed,
          // we should consider removing the suffix if possible when the next time locale change.
          let buttonTextBundleKey = AppConstants.platform == "macosx" ?
            "autocompleteFooterOptionOSXShort" : "autocompleteFooterOptionShort";
          let buttonText = this._stringBundle.GetStringFromName(buttonTextBundleKey);
          this._optionButton.textContent = buttonText;

          let value = JSON.parse(this.getAttribute("ac-value"));

          this._allFieldCategories = value.categories;
          this._focusedCategory = value.focusedCategory;
          this.showWarningText = this._allFieldCategories && this._focusedCategory;

          if (this.showWarningText) {
            messageManager.addMessageListener("FormAutofill:UpdateWarningMessage", this._updateWarningNote);

            this._updateWarningNote();
          } else {
            this._itemBox.setAttribute("no-warning", "true");
          }
        ]]>
        </body>
      </method>
    </implementation>
  </binding>

  <binding id="autocomplete-creditcard-insecure-field" extends="chrome://formautofill/content/formautofill.xml#autocomplete-profile-listitem-base">
    <xbl:content xmlns="http://www.w3.org/1999/xhtml">
      <div anonid="autofill-item-box" class="autofill-insecure-item">
      </div>
    </xbl:content>

    <implementation implements="nsIDOMXULSelectControlItemElement">
      <constructor>
      <![CDATA[
        this._itemBox = document.getAnonymousElementByAttribute(
          this, "anonid", "autofill-item-box"
        );

        this._adjustAcItem();
      ]]>
      </constructor>

      <property name="selected" onget="return this.getAttribute('selected') == 'true';">
        <setter><![CDATA[
          // Make this item unselectable since we see this item as a pure message.
          return false;
        ]]></setter>
      </property>

      <method name="_adjustAcItem">
        <body>
        <![CDATA[
          this._adjustAutofillItemLayout();
          this.setAttribute("formautofillattached", "true");

          let value = this.getAttribute("ac-value");
          this._itemBox.textContent = value;
        ]]>
        </body>
      </method>

    </implementation>
  </binding>

  <binding id="autocomplete-profile-listitem-clear-button" extends="chrome://formautofill/content/formautofill.xml#autocomplete-profile-listitem-base">
    <xbl:content xmlns="http://www.w3.org/1999/xhtml">
      <div anonid="autofill-item-box" class="autofill-item-box autofill-footer">
        <div anonid="autofill-clear-button" class="autofill-footer-row autofill-button"></div>
      </div>
    </xbl:content>

    <handlers>
      <handler event="click" button="0"><![CDATA[
        /* global Cu */
        let {AutoCompletePopup} = ChromeUtils.import("resource://gre/modules/AutoCompletePopup.jsm", {});

        AutoCompletePopup.sendMessageToBrowser("FormAutofill:ClearForm");
      ]]></handler>
    </handlers>

    <implementation implements="nsIDOMXULSelectControlItemElement">
      <constructor>
      <![CDATA[
        this._itemBox = document.getAnonymousElementByAttribute(
          this, "anonid", "autofill-item-box"
        );
        this._clearBtn = document.getAnonymousElementByAttribute(
          this, "anonid", "autofill-clear-button"
        );

        this._adjustAcItem();
      ]]>
      </constructor>

      <method name="_adjustAcItem">
        <body>
        <![CDATA[
          this._adjustAutofillItemLayout();
          this.setAttribute("formautofillattached", "true");

          let clearFormBtnLabel = this._stringBundle.GetStringFromName("clearFormBtnLabel2");
          this._clearBtn.textContent = clearFormBtnLabel;
        ]]>
        </body>
      </method>

    </implementation>
  </binding>

</bindings>
PK
!<¡iet™™"chrome/content/formfill-anchor.svg<!-- 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/. -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" fill="context-fill" fill-opacity="context-fill-opacity">
  <path d="M7.3 6h1.5c.1 0 .2-.1.2-.3V2c0-.5-.4-1-1-1s-1 .4-1 1v3.8c0 .1.1.2.3.2z"/>
  <path d="M13.5 3H11c-.6 0-1 .4-1 1s.4 1 1 1h2.5c.3 0 .5.2.5.5v7c0 .3-.2.5-.5.5h-11c-.3 0-.5-.3-.5-.5v-7c0-.3.2-.5.5-.5H5c.6 0 1-.4 1-1s-.4-1-1-1H2.5C1.1 3 0 4.1 0 5.5v7C0 13.8 1.1 15 2.5 15h11c1.4 0 2.5-1.1 2.5-2.5v-7C16 4.1 14.9 3 13.5 3z"/>
  <path d="M3.6 7h2.8c.3 0 .6.2.6.5v2.8c0 .4-.3.7-.6.7H3.6c-.3 0-.6-.3-.6-.6V7.5c0-.3.3-.5.6-.5zM9.5 8h3c.3 0 .5-.3.5-.5s-.2-.5-.5-.5h-3c-.3 0-.5.2-.5.5s.2.5.5.5zM9.5 9c-.3 0-.5.2-.5.5s.2.5.5.5h2c.3 0 .5-.2.5-.5s-.2-.5-.5-.5h-2z"/>
</svg>
PK
!<ÿÙæª!ª!"chrome/content/heuristicsRegexp.js/* 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/. */

/*
 * Form Autofill field Heuristics RegExp.
 */

/* exported HeuristicsRegExp */

"use strict";

var HeuristicsRegExp = {
  // These regular expressions are from Chromium source codes [1]. Most of them
  // converted to JS format have the same meaning with the original ones except
  // the first line of "address-level1".
  // [1] https://cs.chromium.org/chromium/src/components/autofill/core/browser/autofill_regex_constants.cc
  RULES: {
    // ==== Email ====
    "email": new RegExp(
      "e.?mail" +
      "|courriel" + // fr
      "|メールアドレス" + // ja-JP
      "|Электронной.?Почты" + // ru
      "|邮件|邮箱" + // zh-CN
      "|電郵地址" + // zh-TW
      "|(?:이메일|전자.?우편|[Ee]-?mail)(.?주소)?", // ko-KR
      "iu"
    ),

    // ==== Telephone ====
    "tel": new RegExp(
      "phone|mobile|contact.?number" +
      "|telefonnummer" + // de-DE
      "|telefono|teléfono" + // es
      "|telfixe" + // fr-FR
      "|電話" + // ja-JP
      "|telefone|telemovel" + // pt-BR, pt-PT
      "|телефон" + // ru
      "|电话" + // zh-CN
      "|(?:전화|핸드폰|휴대폰|휴대전화)(?:.?번호)?", // ko-KR
      "iu"
    ),

    // ==== Address Fields ====
    "organization": new RegExp(
      "company|business|organization|organisation" +
      "|firma|firmenname" + // de-DE
      "|empresa" + // es
      "|societe|société" + // fr-FR
      "|ragione.?sociale" + // it-IT
      "|会社" + // ja-JP
      "|название.?компании" + // ru
      "|单位|公司" + // zh-CN
      "|회사|직장", // ko-KR
      "iu"
    ),
    "street-address": new RegExp(
      "streetaddress|street-address",
      "iu"
    ),
    "address-line1": new RegExp(
      "^address$|address[_-]?line(one)?|address1|addr1|street" +
      "|addrline1|address_1" + // Extra rules by Firefox
      "|(?:shipping|billing)address$" +
      "|strasse|straße|hausnummer|housenumber" + // de-DE
      "|house.?name" + // en-GB
      "|direccion|dirección" + // es
      "|adresse" + // fr-FR
      "|indirizzo" + // it-IT
      "|^住所$|住所1" + // ja-JP
      "|morada|endereço" + // pt-BR, pt-PT
      "|Адрес" + // ru
      "|地址" + // zh-CN
      "|^주소.?$|주소.?1", // ko-KR
      "iu"
    ),
    "address-line2": new RegExp(
      "address[_-]?line(2|two)|address2|addr2|street|suite|unit" +
      "|addrline2|address_2" + // Extra rules by Firefox
      "|adresszusatz|ergänzende.?angaben" + // de-DE
      "|direccion2|colonia|adicional" + // es
      "|addresssuppl|complementnom|appartement" + // fr-FR
      "|indirizzo2" + // it-IT
      "|住所2" + // ja-JP
      "|complemento|addrcomplement" + // pt-BR, pt-PT
      "|Улица" + // ru
      "|地址2" + // zh-CN
      "|주소.?2", // ko-KR
      "iu"
    ),
    "address-line3": new RegExp(
      "address[_-]?line(3|three)|address3|addr3|street|suite|unit" +
      "|addrline3|address_3" + // Extra rules by Firefox
      "|adresszusatz|ergänzende.?angaben" + // de-DE
      "|direccion3|colonia|adicional" + // es
      "|addresssuppl|complementnom|appartement" + // fr-FR
      "|indirizzo3" + // it-IT
      "|住所3" + // ja-JP
      "|complemento|addrcomplement" + // pt-BR, pt-PT
      "|Улица" + // ru
      "|地址3" + // zh-CN
      "|주소.?3", // ko-KR
      "iu"
    ),
    "address-level2": new RegExp(
      "city|town" +
      "|\\bort\\b|stadt" + // de-DE
      "|suburb" + // en-AU
      "|ciudad|provincia|localidad|poblacion" + // es
      "|ville|commune" + // fr-FR
      "|localita" + // it-IT
      "|市区町村" + // ja-JP
      "|cidade" + // pt-BR, pt-PT
      "|Город" + // ru
      "|市" + // zh-CN
      "|分區" + // zh-TW
      "|^시[^도·・]|시[·・]?군[·・]?구", // ko-KR
      "iu"
    ),
    "address-level1": new RegExp(
      // JS does not support backward matching, so the following pattern is
      // applied in FormAutofillHeuristics.getInfo() rather than regexp.
      // "(?<!united )state|county|region|province"
      "state|county|region|province" +
      "|land" + // de-DE
      "|county|principality" + // en-UK
      "|都道府県" + // ja-JP
      "|estado|provincia" + // pt-BR, pt-PT
      "|область" + // ru
      "|省" + // zh-CN
      "|地區" + // zh-TW
      "|^시[·・]?도", // ko-KR
      "iu"
    ),
    "postal-code": new RegExp(
      "zip|postal|post.*code|pcode" +
      "|pin.?code" + // en-IN
      "|postleitzahl" + // de-DE
      "|\\bcp\\b" + // es
      "|\\bcdp\\b" + // fr-FR
      "|\\bcap\\b" + // it-IT
      "|郵便番号" + // ja-JP
      "|codigo|codpos|\\bcep\\b" + // pt-BR, pt-PT
      "|Почтовый.?Индекс" + // ru
      "|邮政编码|邮编" + // zh-CN
      "|郵遞區號" + // zh-TW
      "|우편.?번호", // ko-KR
      "iu"
    ),
    "country": new RegExp(
      "country|countries" +
      "|país|pais" + // es
      "|国" + // ja-JP
      "|国家" + // zh-CN
      "|국가|나라", // ko-KR
      "iu"
    ),

    // ==== Name Fields ====
    "name": new RegExp(
      "^name|full.?name|your.?name|customer.?name|bill.?name|ship.?name" +
      "|name.*first.*last|firstandlastname" +
      "|nombre.*y.*apellidos" + // es
      "|^nom" + // fr-FR
      "|お名前|氏名" + // ja-JP
      "|^nome" + // pt-BR, pt-PT
      "|姓名" + // zh-CN
      "|성명", // ko-KR
      "iu"
    ),
    "given-name": new RegExp(
      "first.*name|initials|fname|first$|given.*name" +
      "|vorname" + // de-DE
      "|nombre" + // es
      "|forename|prénom|prenom" + // fr-FR
      "|名" + // ja-JP
      "|nome" + // pt-BR, pt-PT
      "|Имя" + // ru
      "|이름", // ko-KR
      "iu"
    ),
    "additional-name": new RegExp(
      "middle.*name|mname|middle$" +
      "|apellido.?materno|lastlastname" + // es

      // This rule is for middle initial.
      "middle.*initial|m\\.i\\.|mi$|\\bmi\\b",
      "iu"
    ),
    "family-name": new RegExp(
      "last.*name|lname|surname|last$|secondname|family.*name" +
      "|nachname" + // de-DE
      "|apellido" + // es
      "|famille|^nom" + // fr-FR
      "|cognome" + // it-IT
      "|姓" + // ja-JP
      "|morada|apelidos|surename|sobrenome" + // pt-BR, pt-PT
      "|Фамилия" + // ru
      "|\\b성(?:[^명]|\\b)", // ko-KR
      "iu"
    ),

    // ==== Credit Card Fields ====
    "cc-name": new RegExp(
      "card.?(?:holder|owner)|name.*(\\b)?on(\\b)?.*card" +
      "|(?:card|cc).?name|cc.?full.?name" +
      "|karteninhaber" + // de-DE
      "|nombre.*tarjeta" + // es
      "|nom.*carte" + // fr-FR
      "|nome.*cart" + // it-IT
      "|名前" + // ja-JP
      "|Имя.*карты" + // ru
      "|信用卡开户名|开户名|持卡人姓名" + // zh-CN
      "|持卡人姓名", // zh-TW
      "iu"
    ),
    "cc-number": new RegExp(
      "(add)?(?:card|cc|acct).?(?:number|#|no|num|field)" +
      "|(cc|kk)nr" + // Extra rules by Firefox for de-DE
      "|nummer" + // de-DE
      "|credito|numero|número" + // es
      "|numéro" + // fr-FR
      "|カード番号" + // ja-JP
      "|Номер.*карты" + // ru
      "|信用卡号|信用卡号码" + // zh-CN
      "|信用卡卡號" + // zh-TW
      "|카드", // ko-KR
      "iu"
    ),
    "cc-exp-month": new RegExp(
      "expir|exp.*mo|exp.*date|ccmonth|cardmonth|addmonth" +
      "|(cc|kk)month" + // Extra rules by Firefox for de-DE
      "|gueltig|gültig|monat" + // de-DE
      "|fecha" + // es
      "|date.*exp" + // fr-FR
      "|scadenza" + // it-IT
      "|有効期限" + // ja-JP
      "|validade" + // pt-BR, pt-PT
      "|Срок действия карты" + // ru
      "|月", // zh-CN,
      "iu"
    ),
    "cc-exp-year": new RegExp(
      "exp|^/|(add)?year" +
      "|(cc|kk)year" + // Extra rules by Firefox for de-DE
      "|ablaufdatum|gueltig|gültig|jahr" + // de-DE
      "|fecha" + // es
      "|scadenza" + // it-IT
      "|有効期限" + // ja-JP
      "|validade" + // pt-BR, pt-PT
      "|Срок действия карты" + // ru
      "|年|有效期", // zh-CN
      "iu"
    ),
    "cc-exp": new RegExp(
      "expir|exp.*date|^expfield$" +
      "|gueltig|gültig" + // de-DE
      "|fecha" + // es
      "|date.*exp" + // fr-FR
      "|scadenza" + // it-IT
      "|有効期限" + // ja-JP
      "|validade" + // pt-BR, pt-PT
      "|Срок действия карты", // ru
      "iu"
    ),
  },
};
PK
!<?Ó<Ñmm$chrome/content/icon-address-save.svg<!-- 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/. -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
  <path fill="#999899" d="M22 13.7H9.4c-.6 0-1.2.5-1.2 1.2 0 .6.5 1.2 1.2 1.2H22c.6 0 1.2-.5 1.2-1.2s-.6-1.2-1.2-1.2zM6.1 26.6V5.5c0-.8.7-1.5 1.5-1.5h16c.9 0 1.5.6 1.5 1.5V16h2V3.8c0-1-.7-1.8-1.8-1.8H5.9c-1 0-1.8.8-1.8 1.8v24.5c0 1 .8 1.7 1.8 1.7h9.3v-2H7.6c-.8 0-1.5-.6-1.5-1.4zm21.1-1.9h-2.5V20c0-.4-.3-.8-.8-.8h-3.1c-.4 0-.8.3-.8.8v4.6h-2.5c-.6 0-.8.4-.3.8l4.3 4.2c.2.2.5.3.8.3s.6-.1.8-.3l4.3-4.2c.6-.4.4-.7-.2-.7zm-11.3-5.6H9.4c-.6 0-1.2.5-1.2 1.2s.5 1.2 1.2 1.2h6.5c.6 0 1.2-.5 1.2-1.2s-.6-1.2-1.2-1.2zM22 7.8H9.4c-.6 0-1.2.5-1.2 1.2s.5 1.2 1.2 1.2H22c.6 0 1.2-.5 1.2-1.2s-.6-1.2-1.2-1.2z"/>
</svg>
PK
!<0³Zqjj&chrome/content/icon-address-update.svg<!-- 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/. -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
  <path fill="#999899" d="M22 13.7H9.4c-.6 0-1.2.5-1.2 1.2 0 .6.5 1.2 1.2 1.2H22c.6 0 1.2-.5 1.2-1.2s-.6-1.2-1.2-1.2zM6.1 26.6V5.5c0-.8.7-1.5 1.5-1.5h16c.9 0 1.5.6 1.5 1.5V16h2V3.8c0-1-.7-1.8-1.8-1.8H5.9c-1 0-1.8.8-1.8 1.8v24.5c0 1 .8 1.7 1.8 1.7h9.3v-2H7.6c-.8 0-1.5-.6-1.5-1.4zm9.8-7.5H9.4c-.6 0-1.2.5-1.2 1.2s.5 1.2 1.2 1.2h6.5c.6 0 1.2-.5 1.2-1.2s-.6-1.2-1.2-1.2zM22 7.8H9.4c-.6 0-1.2.5-1.2 1.2s.5 1.2 1.2 1.2H22c.6 0 1.2-.5 1.2-1.2s-.6-1.2-1.2-1.2zm-5.7 16l4.4-4.3c.2-.2.5-.3.8-.3s.6.1.8.3l4.4 4.3c.5.5.3.8-.3.8h-2.6v4.7c0 .4-.4.8-.8.8h-3c-.4 0-.8-.4-.8-.8v-4.7h-2.5c-.7 0-.8-.4-.4-.8z"/>
</svg>
PK
!<QÒù!¶¶+chrome/content/icon-credit-card-generic.svg<!-- 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/. -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
  <path fill="context-fill" d="M4.5,9.4H3.2c-0.3,0-0.5,0.2-0.5,0.5s0.2,0.5,0.5,0.5h1.3c0.3,0,0.5-0.2,0.5-0.5S4.8,9.4,4.5,9.4z"/>
  <path fill="context-fill" d="M9.3,9.4H6.2c-0.3,0-0.5,0.2-0.5,0.5s0.2,0.5,0.5,0.5h3.2c0.3,0,0.5-0.2,0.5-0.5S9.6,9.4,9.3,9.4z"/>
  <path fill="context-fill" d="M14,2H2C0.9,2,0,2.9,0,4v8c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2V4C16,2.9,15.1,2,14,2z M14,12H2V7.7h12V12z
	  M14,6H2V4h12V6z"/>
</svg>
PK
!<ó§rýý#chrome/content/icon-credit-card.svg<!-- 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/. -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
  <path fill="#4E4D4D" d="M9 22.2H6.4c-.6 0-1 .4-1 1s.4 1 1 1H9c.6 0 1-.4 1-1s-.4-1-1-1z"/>
  <path fill="#4E4D4D" d="M28 7.6v8H4v-4h10v-4H4c-2.2 0-4 1.8-4 4v16c0 2.2 1.8 4 4 4h24c2.2 0 4-1.8 4-4v-16c0-2.2-1.8-4-4-4zm-24 20V19h24v8.6H4z"/>
  <path fill="#4E4D4D" d="M19.2 22.2h-6.3c-.6 0-1 .4-1 1s.4 1 1 1h6.3c.6 0 1-.4 1-1s-.5-1-1-1zM16.3 7.9c-.4.4-.4 1 0 1.4l4 4c.4.4 1 .4 1.4 0l4-4c.4-.4.4-1 0-1.4s-1-.4-1.4 0L22 10.2v-9c0-.5-.4-1-1-1-.5 0-1 .4-1 1v9l-2.3-2.3c-.4-.4-1-.4-1.4 0z"/>
</svg>
PK
!</§²ôôchrome/content/l10n.js/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

/**
 * This file will be replaced by Fluent but it's a middle ground so we can share
 * the edit dialog code with the unprivileged PaymentRequest dialog before the
 * Fluent conversion
 */

/* global content */

ChromeUtils.import("resource://formautofill/FormAutofillUtils.jsm");

const CONTENT_WIN = typeof(window) != "undefined" ? window : this;

const L10N_ATTRIBUTES = ["data-localization", "data-localization-region"];

// eslint-disable-next-line mozilla/balanced-listeners
CONTENT_WIN.addEventListener("DOMContentLoaded", function onDCL(evt) {
  let doc = evt.target;
  FormAutofillUtils.localizeMarkup(doc);

  let mutationObserver = new doc.ownerGlobal.MutationObserver(function onMutation(mutations) {
    for (let mutation of mutations) {
      switch (mutation.type) {
        case "attributes": {
          if (!mutation.target.hasAttribute(mutation.attributeName)) {
            // The attribute was removed in the meantime.
            continue;
          }
          FormAutofillUtils.localizeAttributeForElement(mutation.target, mutation.attributeName);
          break;
        }

        case "childList": {
          // We really only care about elements appending inside pages.
          if (!mutation.addedNodes || !mutation.target.closest(".page")) {
            break;
          }
          FormAutofillUtils.localizeMarkup(mutation.target);
          break;
        }
      }
    }
  });

  mutationObserver.observe(doc, {
    attributes: true,
    attributeFilter: L10N_ATTRIBUTES,
    childList: true,
    subtree: true,
  });
});
PK
!<¤(¾:^^$chrome/content/manageAddresses.xhtml<?xml version="1.0" encoding="UTF-8"?>
<!-- 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/. -->
<!DOCTYPE html [
  <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
  %globalDTD;
]>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <title data-localization="manageAddressesTitle"/>
  <link rel="stylesheet" href="chrome://global/skin/in-content/common.css" />
  <link rel="stylesheet" href="chrome://formautofill/content/manageDialog.css" />
  <script src="chrome://formautofill/content/manageDialog.js"></script>
</head>
<body dir="&locale.dir;">
  <fieldset>
    <legend data-localization="addressesListHeader"/>
    <select id="addresses" size="9" multiple="multiple"/>
  </fieldset>
  <div id="controls-container">
    <button id="remove" disabled="disabled" data-localization="removeBtnLabel"/>
    <!-- Wrapper is used to properly compute the search tooltip position -->
    <div>
      <button id="add" data-localization="addBtnLabel"/>
    </div>
    <button id="edit" disabled="disabled" data-localization="editBtnLabel"/>
  </div>
  <script type="application/javascript">
    "use strict";
    /* global ManageAddresses */
    new ManageAddresses({
      records: document.getElementById("addresses"),
      controlsContainer: document.getElementById("controls-container"),
      remove: document.getElementById("remove"),
      add: document.getElementById("add"),
      edit: document.getElementById("edit"),
    });
  </script>
</body>
</html>
PK
!<¿ÔL<ll&chrome/content/manageCreditCards.xhtml<?xml version="1.0" encoding="UTF-8"?>
<!-- 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/. -->
<!DOCTYPE html [
  <!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
  %globalDTD;
]>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <title data-localization="manageCreditCardsTitle"/>
  <link rel="stylesheet" href="chrome://global/skin/in-content/common.css" />
  <link rel="stylesheet" href="chrome://formautofill/content/manageDialog.css" />
  <script src="chrome://formautofill/content/manageDialog.js"></script>
</head>
<body dir="&locale.dir;">
  <fieldset>
    <legend data-localization="creditCardsListHeader"/>
    <select id="credit-cards" size="9" multiple="multiple"/>
  </fieldset>
  <div id="controls-container">
    <button id="remove" disabled="disabled" data-localization="removeBtnLabel"/>
    <!-- Wrapper is used to properly compute the search tooltip position -->
    <div>
      <button id="add" data-localization="addBtnLabel"/>
    </div>
    <button id="edit" disabled="disabled" data-localization="editBtnLabel"/>
  </div>
  <script type="application/javascript">
    "use strict";
    /* global ManageCreditCards */
    new ManageCreditCards({
      records: document.getElementById("credit-cards"),
      controlsContainer: document.getElementById("controls-container"),
      remove: document.getElementById("remove"),
      add: document.getElementById("add"),
      edit: document.getElementById("edit"),
    });
  </script>
</body>
</html>
PK
!<t¬chrome/content/manageDialog.css/* 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/. */

html {
  /* Prevent unnecessary horizontal scroll bar from showing */
  overflow-x: hidden;
}

div {
  display: flex;
}

button {
  padding-right: 10px;
  padding-left: 10px;
}

fieldset {
  margin: 0;
  padding: 0;
  border: none;
}

fieldset > legend {
  box-sizing: border-box;
  width: 100%;
  padding: 0.4em 0.7em;
  color: #808080;
  background-color: var(--in-content-box-background-hover);
  border: 1px solid var(--in-content-box-border-color);
  border-radius: 2px 2px 0 0;
  -moz-user-select: none;
}

option:nth-child(even) {
  background-color: -moz-oddtreerow;
}

#addresses,
#credit-cards {
  width: 100%;
  height: 16.6em;
  margin: 0;
  border-top: none;
  border-radius: 0 0 2px 2px;
}

#addresses > option,
#credit-cards > option {
  display: flex;
  align-items: center;
  height: 1.6em;
  padding-inline-start: 0.6em;
}

#controls-container {
  flex: 0 1 100%;
  justify-content: end;
  margin-top: 1em;
}

#remove {
  margin-inline-start: 0;
  margin-inline-end: auto;
}

#edit {
  margin-inline-end: 0;
}

#credit-cards > option::before {
  content: "";
  background: url("icon-credit-card-generic.svg") no-repeat;
  background-size: contain;
  float: left;
  width: 16px;
  height: 16px;
  padding-inline-end: 10px;
}

/*
  We use .png / @2x.png images where we don't yet have a vector version of a logo
*/
#credit-cards.branded > option[cc-type="amex"]::before {
  background-image: url("third-party/cc-logo-amex.png");
}

#credit-cards.branded > option[cc-type="cartebancaire"]::before {
  background-image: url("third-party/cc-logo-cartebancaire.png");
}

#credit-cards.branded > option[cc-type="diners"]::before {
  background-image: url("third-party/cc-logo-diners.svg");
}

#credit-cards.branded > option[cc-type="discover"]::before {
  background-image: url("third-party/cc-logo-discover.png");
}

#credit-cards.branded > option[cc-type="jcb"]::before {
  background-image: url("third-party/cc-logo-jcb.svg");
}

#credit-cards.branded > option[cc-type="mastercard"]::before {
  background-image: url("third-party/cc-logo-mastercard.svg");
}

#credit-cards.branded > option[cc-type="mir"]::before {
  background-image: url("third-party/cc-logo-mir.svg");
}

#credit-cards.branded > option[cc-type="unionpay"]::before {
  background-image: url("third-party/cc-logo-unionpay.svg");
}

#credit-cards.branded > option[cc-type="visa"]::before {
  background-image: url("third-party/cc-logo-visa.svg");
}

@media (min-resolution: 1.1dppx) {
  #credit-cards.branded > option[cc-type="amex"]::before {
    background-image: url("third-party/cc-logo-amex@2x.png");
  }
  #credit-cards.branded > option[cc-type="cartebancaire"]::before {
    background-image: url("third-party/cc-logo-cartebancaire@2x.png");
  }
  #credit-cards.branded > option[cc-type="discover"]::before {
    background-image: url("third-party/cc-logo-discover@2x.png");
  }
}
PK
!<äF—pÿ1ÿ1chrome/content/manageDialog.js/* 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 ManageAddresses, ManageCreditCards */

"use strict";

const EDIT_ADDRESS_URL = "chrome://formautofill/content/editAddress.xhtml";
const EDIT_CREDIT_CARD_URL = "chrome://formautofill/content/editCreditCard.xhtml";

ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.import("resource://formautofill/FormAutofill.jsm");

ChromeUtils.defineModuleGetter(this, "CreditCard",
                               "resource://gre/modules/CreditCard.jsm");
ChromeUtils.defineModuleGetter(this, "formAutofillStorage",
                               "resource://formautofill/FormAutofillStorage.jsm");
ChromeUtils.defineModuleGetter(this, "FormAutofillUtils",
                               "resource://formautofill/FormAutofillUtils.jsm");
ChromeUtils.defineModuleGetter(this, "OSKeyStore",
                               "resource://formautofill/OSKeyStore.jsm");

XPCOMUtils.defineLazyGetter(this, "reauthPasswordPromptMessage", () => {
  const brandShortName = FormAutofillUtils.brandBundle.GetStringFromName("brandShortName");
  return FormAutofillUtils.stringBundle.formatStringFromName(
    `editCreditCardPasswordPrompt.${AppConstants.platform}`, [brandShortName], 1);
});

this.log = null;
FormAutofill.defineLazyLogGetter(this, "manageAddresses");

class ManageRecords {
  constructor(subStorageName, elements) {
    this._storageInitPromise = formAutofillStorage.initialize();
    this._subStorageName = subStorageName;
    this._elements = elements;
    this._newRequest = false;
    this._isLoadingRecords = false;
    this.prefWin = window.opener;
    this.localizeDocument();
    window.addEventListener("DOMContentLoaded", this, {once: true});
  }

  async init() {
    await this.loadRecords();
    this.attachEventListeners();
    // For testing only: Notify when the dialog is ready for interaction
    window.dispatchEvent(new CustomEvent("FormReady"));
  }

  uninit() {
    log.debug("uninit");
    this.detachEventListeners();
    this._elements = null;
  }

  localizeDocument() {
    document.documentElement.style.minWidth = FormAutofillUtils.stringBundle.GetStringFromName("manageDialogsWidth");
    FormAutofillUtils.localizeMarkup(document);
  }

  /**
   * Get the selected options on the addresses element.
   *
   * @returns {array<DOMElement>}
   */
  get _selectedOptions() {
    return Array.from(this._elements.records.selectedOptions);
  }

  /**
   * Get storage and ensure it has been initialized.
   * @returns {object}
   */
  async getStorage() {
    await this._storageInitPromise;
    return formAutofillStorage[this._subStorageName];
  }

  /**
   * Load records and render them. This function is a wrapper for _loadRecords
   * to ensure any reentrant will be handled well.
   */
  async loadRecords() {
    // This function can be early returned when there is any reentrant happends.
    // "_newRequest" needs to be set to ensure all changes will be applied.
    if (this._isLoadingRecords) {
      this._newRequest = true;
      return;
    }
    this._isLoadingRecords = true;

    await this._loadRecords();

    // _loadRecords should be invoked again if there is any multiple entrant
    // during running _loadRecords(). This step ensures that the latest request
    // still is applied.
    while (this._newRequest) {
      this._newRequest = false;
      await this._loadRecords();
    }
    this._isLoadingRecords = false;

    // For testing only: Notify when records are loaded
    this._elements.records.dispatchEvent(new CustomEvent("RecordsLoaded"));
  }

  async _loadRecords() {
    let storage = await this.getStorage();
    let records = await storage.getAll();
    // Sort by last used time starting with most recent
    records.sort((a, b) => {
      let aLastUsed = a.timeLastUsed || a.timeLastModified;
      let bLastUsed = b.timeLastUsed || b.timeLastModified;
      return bLastUsed - aLastUsed;
    });
    await this.renderRecordElements(records);
    this.updateButtonsStates(this._selectedOptions.length);
  }

  /**
   * Render the records onto the page while maintaining selected options if
   * they still exist.
   *
   * @param  {array<object>} records
   */
  async renderRecordElements(records) {
    let selectedGuids = this._selectedOptions.map(option => option.value);
    this.clearRecordElements();
    for (let record of records) {
      let option = new Option(this.getLabel(record),
                              record.guid,
                              false,
                              selectedGuids.includes(record.guid));
      option.record = record;
      this._elements.records.appendChild(option);
    }
  }

  /**
   * Remove all existing record elements.
   */
  clearRecordElements() {
    let parent = this._elements.records;
    while (parent.lastChild) {
      parent.removeChild(parent.lastChild);
    }
  }

  /**
   * Remove records by selected options.
   *
   * @param  {array<DOMElement>} options
   */
  async removeRecords(options) {
    let storage = await this.getStorage();
    // Pause listening to storage change event to avoid triggering `loadRecords`
    // when removing records
    Services.obs.removeObserver(this, "formautofill-storage-changed");

    for (let option of options) {
      storage.remove(option.value);
      option.remove();
    }
    this.updateButtonsStates(this._selectedOptions);

    // Resume listening to storage change event
    Services.obs.addObserver(this, "formautofill-storage-changed");
    // For testing only: notify record(s) has been removed
    this._elements.records.dispatchEvent(new CustomEvent("RecordsRemoved"));
  }

  /**
   * Enable/disable the Edit and Remove buttons based on number of selected
   * options.
   *
   * @param  {number} selectedCount
   */
  updateButtonsStates(selectedCount) {
    log.debug("updateButtonsStates:", selectedCount);
    if (selectedCount == 0) {
      this._elements.edit.setAttribute("disabled", "disabled");
      this._elements.remove.setAttribute("disabled", "disabled");
    } else if (selectedCount == 1) {
      this._elements.edit.removeAttribute("disabled");
      this._elements.remove.removeAttribute("disabled");
    } else if (selectedCount > 1) {
      this._elements.edit.setAttribute("disabled", "disabled");
      this._elements.remove.removeAttribute("disabled");
    }
  }

  /**
   * Handle events
   *
   * @param  {DOMEvent} event
   */
  handleEvent(event) {
    switch (event.type) {
      case "DOMContentLoaded": {
        this.init();
        break;
      }
      case "click": {
        this.handleClick(event);
        break;
      }
      case "change": {
        this.updateButtonsStates(this._selectedOptions.length);
        break;
      }
      case "unload": {
        this.uninit();
        break;
      }
      case "keypress": {
        this.handleKeyPress(event);
        break;
      }
      case "contextmenu": {
        event.preventDefault();
        break;
      }
    }
  }

  /**
   * Handle click events
   *
   * @param  {DOMEvent} event
   */
  handleClick(event) {
    if (event.target == this._elements.remove) {
      this.removeRecords(this._selectedOptions);
    } else if (event.target == this._elements.add) {
      this.openEditDialog();
    } else if (event.target == this._elements.edit ||
               event.target.parentNode == this._elements.records && event.detail > 1) {
      this.openEditDialog(this._selectedOptions[0].record);
    }
  }

  /**
   * Handle key press events
   *
   * @param  {DOMEvent} event
   */
  handleKeyPress(event) {
    if (event.keyCode == KeyEvent.DOM_VK_ESCAPE) {
      window.close();
    }
    if (event.keyCode == KeyEvent.DOM_VK_DELETE) {
      this.removeRecords(this._selectedOptions);
    }
  }

  observe(subject, topic, data) {
    switch (topic) {
      case "formautofill-storage-changed": {
        this.loadRecords();
      }
    }
  }

  /**
   * Attach event listener
   */
  attachEventListeners() {
    window.addEventListener("unload", this, {once: true});
    window.addEventListener("keypress", this);
    window.addEventListener("contextmenu", this);
    this._elements.records.addEventListener("change", this);
    this._elements.records.addEventListener("click", this);
    this._elements.controlsContainer.addEventListener("click", this);
    Services.obs.addObserver(this, "formautofill-storage-changed");
  }

  /**
   * Remove event listener
   */
  detachEventListeners() {
    window.removeEventListener("keypress", this);
    window.removeEventListener("contextmenu", this);
    this._elements.records.removeEventListener("change", this);
    this._elements.records.removeEventListener("click", this);
    this._elements.controlsContainer.removeEventListener("click", this);
    Services.obs.removeObserver(this, "formautofill-storage-changed");
  }
}

class ManageAddresses extends ManageRecords {
  constructor(elements) {
    super("addresses", elements);
    elements.add.setAttribute("searchkeywords", FormAutofillUtils.EDIT_ADDRESS_KEYWORDS
                                                  .map(key => FormAutofillUtils.stringBundle.GetStringFromName(key))
                                                  .join("\n"));
  }

  /**
   * Open the edit address dialog to create/edit an address.
   *
   * @param  {object} address [optional]
   */
  openEditDialog(address) {
    this.prefWin.gSubDialog.open(EDIT_ADDRESS_URL, null, {
      record: address,
      // Don't validate in preferences since it's fine for fields to be missing
      // for autofill purposes. For PaymentRequest addresses get more validation.
      noValidate: true,
    });
  }

  getLabel(address) {
    return FormAutofillUtils.getAddressLabel(address);
  }
}

class ManageCreditCards extends ManageRecords {
  constructor(elements) {
    super("creditCards", elements);
    elements.add.setAttribute("searchkeywords", FormAutofillUtils.EDIT_CREDITCARD_KEYWORDS
                                                  .map(key => FormAutofillUtils.stringBundle.GetStringFromName(key))
                                                  .join("\n"));
    this._isDecrypted = false;
  }

  /**
   * Open the edit address dialog to create/edit a credit card.
   *
   * @param  {object} creditCard [optional]
   */
  async openEditDialog(creditCard) {
    // Ask for reauth if user is trying to edit an existing credit card.
    if (!creditCard || await OSKeyStore.ensureLoggedIn(reauthPasswordPromptMessage)) {
      let decryptedCCNumObj = {};
      if (creditCard && creditCard["cc-number-encrypted"]) {
        try {
          decryptedCCNumObj["cc-number"] = await OSKeyStore.decrypt(creditCard["cc-number-encrypted"]);
        } catch (ex) {
          if (ex.result == Cr.NS_ERROR_ABORT) {
            // User shouldn't be ask to reauth here, but it could happen.
            // Return here and skip opening the dialog.
            return;
          }
          // We've got ourselves a real error.
          // Recover from encryption error so the user gets a chance to re-enter
          // unencrypted credit card number.
          decryptedCCNumObj["cc-number"] = "";
          Cu.reportError(ex);
        }
      }
      let decryptedCreditCard = Object.assign({}, creditCard, decryptedCCNumObj);
      this.prefWin.gSubDialog.open(EDIT_CREDIT_CARD_URL, "resizable=no", {
        record: decryptedCreditCard,
      });
    }
  }

  /**
   * Get credit card display label. It should display masked numbers and the
   * cardholder's name, separated by a comma.
   *
   * @param {object} creditCard
   * @returns {string}
   */
  getLabel(creditCard) {
    return CreditCard.getLabel({
      name: creditCard["cc-name"],
      number: creditCard["cc-number"],
    });
  }

  async renderRecordElements(records) {
    // Revert back to encrypted form when re-rendering happens
    this._isDecrypted = false;
    // Display third-party card icons when possible
    this._elements.records.classList.toggle("branded", AppConstants.MOZILLA_OFFICIAL);
    await super.renderRecordElements(records);

    let options = this._elements.records.options;
    for (let option of options) {
      let record = option.record;
      if (record && record["cc-type"]) {
        option.setAttribute("cc-type", record["cc-type"]);
      } else {
        option.removeAttribute("cc-type");
      }
    }
  }

  updateButtonsStates(selectedCount) {
    super.updateButtonsStates(selectedCount);
  }

  handleClick(event) {
    super.handleClick(event);
  }
}
PK
!<oO6íâ	â	 chrome/content/nameReferences.js/* 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 nameReferences */

"use strict";

// The data below is initially copied from
// https://cs.chromium.org/chromium/src/components/autofill/core/browser/autofill_data_util.cc?rcl=b861deff77abecff11ae6a9f6946e9cc844b9817
var nameReferences = {
  NAME_PREFIXES: [
    "1lt",
    "1st",
    "2lt",
    "2nd",
    "3rd",
    "admiral",
    "capt",
    "captain",
    "col",
    "cpt",
    "dr",
    "gen",
    "general",
    "lcdr",
    "lt",
    "ltc",
    "ltg",
    "ltjg",
    "maj",
    "major",
    "mg",
    "mr",
    "mrs",
    "ms",
    "pastor",
    "prof",
    "rep",
    "reverend",
    "rev",
    "sen",
    "st",
  ],

  NAME_SUFFIXES: [
    "b.a",
    "ba",
    "d.d.s",
    "dds",
    "i",
    "ii",
    "iii",
    "iv",
    "ix",
    "jr",
    "m.a",
    "m.d",
    "ma",
    "md",
    "ms",
    "ph.d",
    "phd",
    "sr",
    "v",
    "vi",
    "vii",
    "viii",
    "x",
  ],

  FAMILY_NAME_PREFIXES: [
    "d'",
    "de",
    "del",
    "der",
    "di",
    "la",
    "le",
    "mc",
    "san",
    "st",
    "ter",
    "van",
    "von",
  ],

  // The common and non-ambiguous CJK surnames (last names) that have more than
  // one character.
  COMMON_CJK_MULTI_CHAR_SURNAMES: [
    // Korean, taken from the list of surnames:
    // https://ko.wikipedia.org/wiki/%ED%95%9C%EA%B5%AD%EC%9D%98_%EC%84%B1%EC%94%A8_%EB%AA%A9%EB%A1%9D
    "남궁",
    "사공",
    "서문",
    "선우",
    "제갈",
    "황보",
    "독고",
    "망절",

    // Chinese, taken from the top 10 Chinese 2-character surnames:
    // https://zh.wikipedia.org/wiki/%E8%A4%87%E5%A7%93#.E5.B8.B8.E8.A6.8B.E7.9A.84.E8.A4.87.E5.A7.93
    // Simplified Chinese (mostly mainland China)
    "欧阳",
    "令狐",
    "皇甫",
    "上官",
    "司徒",
    "诸葛",
    "司马",
    "宇文",
    "呼延",
    "端木",
    // Traditional Chinese (mostly Taiwan)
    "張簡",
    "歐陽",
    "諸葛",
    "申屠",
    "尉遲",
    "司馬",
    "軒轅",
    "夏侯",
  ],

  // All Korean surnames that have more than one character, even the
  // rare/ambiguous ones.
  KOREAN_MULTI_CHAR_SURNAMES: [
    "강전",
    "남궁",
    "독고",
    "동방",
    "망절",
    "사공",
    "서문",
    "선우",
    "소봉",
    "어금",
    "장곡",
    "제갈",
    "황목",
    "황보",
  ],
};
PK
!<ï(O˜+chrome/content/third-party/cc-logo-amex.png‰PNG


IHDR;0®¢áIDATx½WS˜4Iì³m۶mûîålþ¶m۶m[c¯m
–ƒ¸¨ìßìهÉ™ª®ŽDdv¯vfk˳›{—ŸÙؒ95­iÖ2
ZSwb@.áÔšgiM=ÐZr	gÜÄ
ÇIÖÇÙkB®ÿâ$>£‘—µ¶ӸvFc+.kuĺà¬&V!;¿™íÐzû$~Úâ þ˄‹sQ#Ï_'Pßšž‰;»º‘[!kQ£Ûêhÿ˜±Æßú^ùþ:ƒÄôúâ69ؕ7{®¯WÐC\ùÕxaP‚5Q|26
ÏðÉ^ÛeyH-©ÁëÒªâóñúÞóC3ðü@ŸAbFû۬,Ãâ€ö§IpUЏÖw}¡D|U[‡\«p)SŸYVg^5Öû‚Ðþ6IôZC*û_“bF{?Õ
z¯+„öû>ÜÕ͍;:¡ý±ƒ7Á_‘´ßЁk
Ì8“5Wäv•BÙÏ32巬V5=}wd
ªꢸ­‹KÈ&í.eÊóʼnûzxPŽ	ùõí8b{wDн?*åU\Û΁™©W‡&Ckä6D|P³Lez
[ڱ/««<œÍ”có­å¨!9‰U«`ôöb\®Ìtw\‘/çÞ•‚&s¸ï2@ü¯÷öJDÏRÚ?&¹ñÙl“³~â7†'# "fªÕïMÉA|51]¾ŸÓTOûä=¥hCÁ#æq;K°--„Óèû•ŠLÂKƒu<Õß'םÅô.súU)„l[jíü.ŽŠÐJ+#hº(×1(±TTEXãTh?î‘Åb8hլû½¬±öó^ٻZ©ú׽R¶—¨œ{l¥t(ûkn65"Ê?	1½í²*Êf²¾㷗S…£͔]…I;J0eo)¦0“¨d²e.?†o,„ƒ¡ÌÌów•3‹¥'&>h±$÷cӽÂíƒe­Ê0|K±ZŒåžRù:7ßVñtbTíu¸YI:v¯'ñÆäàIk,Š–ðÛ><ÒۃŠꈤ-%kÚ/{$ìw‰øâæ6Y{©W-¦̞[…ØRÚO{ðڨlÃ#Sx†ªV7º¹“KÔýÝÔôXSpý6¢.Ãæ” .ma“ÿšʶ1һ»¹ÅQ­uÀB[E=‰Yÿþ¬êgAf¥Ô1ƒØrÖöâf6iNj[Ú%kOöõÊx�\ý®'±ô¯Ìk¿í•¨ä6†µæ9!¿‚í¤fùR§Ÿgå|=‰ùû-ŽÅ_fféà,nI!ª饬$–Ó9Át´ÍÒ<YeD5²·žÄLÝø]%È.¯;4‰ú­áÉRÓ{º»Ñqe>Nk £nÔBE-ðŸ´ø‰¯mPÅò@8MרÙì-¬G£SEÕ״qˆš•õZ[ gäi¶ÍDÂóâ"®GñÙøý{ŽÎgútð·z‚µã,~Š"z™{;3BØB…3:y²ubä|A`ª3Õ@2ñ£}¼òrˆxøà—ïù0v¤‡p5£Vmõͤtä¸ÎFzi­AbB^è˜N£8[zœû—´<ìe¯•Í8± Aœ8xÖÀëm_è-ü&]r	ç9­ÿžÛÜ3ýì¦Öi‰€âRœÿI£ü¤‰òMIEND®B`‚PK
!<°Ba		.chrome/content/third-party/cc-logo-amex@2x.png‰PNG


IHDR<<:üÙrÎIDATxíZc´+Kͳm۶mÛïó3Žùl۶mãÚ÷0ö±m$õ՞5«’žðbòã¬ÔZu0éšéÝ]]µ«2–ÕªZ£Ä>wÂê9SYX-–œªï,%^²X§¶#°Zr«¾´Úɒ[9¥5X4888ø–
²ܜBo­ˆŒϩĵôô±KÏ6×óÈÎ÷Ûé']IuË;­«Mx½²\KK|ÜI+çWÉóV-¨¢ƒŸHbóL€yÂm`L~ýÒjòwŽQ*ùÝÙO+ðM‹éŒW|”®L†Âtüó޽Śí9¯ù)¦¤Â¾2ð‹随ëÏRT™ø‘O»ÉrÝ"¬ŽOlûÒÌíYüѢîtlMØáv¯¼JZT?DÚæa:„Ýï€ǢôQ'÷œ›Úú'òþ‚n²\ŒÉ]ùn€xġÚA²Ó»Rß=FkWÓ&·×RÇÀAޝ߅11v>å§w™XsM/a# 7~րÝCÀPמú« £!Úú.+ü¢WïÃ`±{1v×/âÅòÈNŸÂ6—¾ Hˆ¯õ{Ë
qìòL
Z¸ùÖ^‚4öŽÓFå5”âÛý!
…r÷O-t,Ñ#(aL\Z”½eD÷Ž.ú¦¦— 5Mô*YŽùiI@À}Æ&Âyè·Vì’¹•Épºø¬¢‡ >pW¿LXbă¿¶dt"Dc“aY4y^&ãa¯Ìê7EZ’I3ØîµÑúe5‘®ùÙއ pŽº°òލ&¦½ùˆMÔDÀ„sØ=4I÷vá<+gûƒ…Ýôïë”ë+ñ¤fù	Ö5dŒ‰oqG-rmĎu^`PÏðЊ9‘ÅY‹ƒن8N9&ÆîÞþC³šjn®Pçõ/w?­ÀÿGÛ]	W‰O¸ð
?ú¤K®nðëF±¹ùó\“£r&GüߒñË0Vq’jí²0(^m¨¢o´Éá¼ÅìR‹§}4!`xÄÙ3û£
÷R˜ÜðxH;>ÛÞÆY ¤®cžu›“ø×¢qñ[L2¨HôúœN|®ؗ}ה0š¿sZÜŸO÷Ð/ö>ś¶¸ӊ¼JiգÍÓÅ…ê¤ö}ÔAºúEJÖ,ª–1˜$8uçàDBé¹ö¤@'#n}ý§õXlŒ‰byõ9è	óƒ@„ä}Õ“VÐA‰æàZ¬—¼0£#!`ñÜ?r” dñ°¨¦ÆjUÝCV¦Š›2Í¢‘¾Š' ½„)1àæ>ÌHD†F‘™4220n†Ü'xò/	*2üŸHĸxßÖö%Ï6ڊ—ܡgа€ñ€禷„S¯®]‰¾ë•ÖÐ?6Óý¿¶ÆÕÿ}\qÊñ8í%oR—†|oíÛ­¬n6°DÃH•òiEҁ²ÿáT’Lv‰bcÐUò´HG 'Œ4´ã}6ÅÞqô³n,–9€¨äÛ&™8O•Œ҂ƒJ!¼Ó-¨œÄ_ñnÝT=ríÂ7ýŠ]Á׍F;	z:`Ž	NÜc9´x@Ý8­¸ÚF	âl¡#˜Հ	iÊîxõûAq¥’º®1„ˆ-=é/˜ÿ­_㉗߬ØU6ÑaODlPi5öŒ+€ÿÉé
sÑƠÅóÔR¶xàbW¾T:Ë[L¸åRµx¤£1îj¢L„–;d¤¼¥ÛáU´!Ü)Fá~|–Ú	ÒÏÁå¬Wᚚ[¥­èZ€jÄîxHôg\✦¤GŒ»Cx®G<iÀ˭çŒÞT±^(ôO¢߄‡ÏÒW<ÿ ]ÿI=î§|¶RN…Vz&’WgwÒ
†¹!˜>üwÑäķ˻ţ¼Õ]‘*F4'µ.¬҂ÞUïcº&k²‡ýhë#£|VÙC«jc”x#ek†Ëq@9˜R0`ʿó_÷+ á2©™ÕøûÛë h1”¥ù_EjæÆމ/3ø&”‘A
r*
¤Ppj¤d’»“žÐ[s…å河‚SÔÆhݢ‚½ñÓz!"sëÇh§»­,]Š¥”ŽG=íV@ã|îú€¶»Ga]RŸOè=¯J¦›[Ýãá¯È,`L}ä÷¸µúÞüøú.ë|Áތ‚Jé@°§›”3mtcÐPñ“¢î¶jLX3´ôï‘ê
‹´ò¯—Ý8žÀÝ÷yØa„PDg>ï>éuƒ	îÎÀ;KaÀé%°éàò«£ÝΘȿžÖW¦ö÷ƒHkÆâ!ó;œ¬FFÎÜ0É7ôèn-÷T¹i/ze±üš7Às2XÎð	/x@+7òѿFwÓ(í(@i<Ãç½æ×ÒÄÛ1J» ”4õ›¥q¯žóÄDéý™Bn~GmL”¾â݀¾ÚæÚæ^Í6ãyM”q©TÙáÁ1æä¯ø°D{?äÀW®ZÓní8¤ãZB¤ɂ†qÚåžçaèj<1|¿´~ir]X?,ß]úV fg·¿Û*õ8ä“Åݴм1­æŒ3-hNz
j‰:adX›pP×6ʋ3;šŒ}2ôÕ2ǥѶÅC1Ñ%ѹ̓oý¢AÚ8¸†û€/ÿ¾³.÷þ܂o•ÿyœØà7Ú<OïeÀcߙ
õði/{é€GåՅ´5l9ßc?ƒÝAüÿU=î)½-¼)ïxEð\/å^Û;”W,Ðâykn§©.-ퟥQœݐñ:Eäù] ò~ÖN÷ؐ‚>÷›ØLù¶¦—ÖÈW£2Üž„×,2ûÚRN%¾½CkÆ4ÅwI‰˜*§̿˜†ÉÜl¢æ,ų¥/}¹tŠjpðTÑ,àÊ,¥~¾à˜Ú
Œ:nqMþº厏×-©þh*+0ëÿmaHujwIEND®B`‚PK
!<+©82ØØ4chrome/content/third-party/cc-logo-cartebancaire.png‰PNG


IHDR$˃+tRNSn¦‘IDATxìŒmØXÄH=oõÛR:³5“o_Ò@Ž!˜ƒøÇßÀÿþÿu'ÜqC–Ý&¿0í†O²Üϻï²qw²ïÝ~㓤‰?±¾彏{³^µ't¡„¢ ¢¢-A(˜7(‹3œÑ
ž˩iD'væõä*7m‰¯­qҔ©”|žB*`m×8,ՓÌbìı™žÃ.ÉOLr…›)	[£eچ”æøFðk©ŽuæëíFäìãȌ›vö‰ÂUX,ÒÖi[îÛߌ”°æf†ßó!¹¶­ڶm۶=¨ÝêvT۶­µí½6Âïl’µîÿ?ëÉ)Y™ŸQ’i)¢¯µwx`ÀŠ8¤b%H 4œ6‰!X$ùf0[Jˆ8—¡³"I¢bˆ9s-uþ.›¶MÃ¥y¶n`–ô
ÿ<kÉ¿MÿgnddâsöÜôò}·âUædeÄ0;Þ“ç?óé}CNœL!Yž3âφᖥù/°÷.եXƒL-šKò¢匝Zîûòÿ‡¾B]Iîž-•Ø8‡ìдCKåi}„̌h€/䁽Š\õþiû¯cZ۪<v·köÛ
&ÁHű»o¾ÓµF+ÖZÄ„€aãݼÇ֛—"w±Siy^Fä@J(-”ç9Ö)[6!
г3ٶÁ3©e=C#ã—tödX°Ÿ´šÛ\˜U“—‹ôس¡\I¨´õ
]õÚϺÉÖКµT`S`kA„õxs⌏¦̕R3°°w¨sÈi}ëf`õÂÉbîsü¿ç¶wv#ËfCd
äˆ	žñ±!h¬ٺ¬˜Vý#âó¦´÷ܲ÷v«®D¬y<âÿ-hà›ñ‹{F /—|€ há@׀ëæÚ6֦"'+Z°6L›W”mo]^„³K}ÙÖUE‡?÷ý°"¹²ô) iôwÿ½tҀ™?™=Ÿ$!û4WíÒ\"(IZ
ˆÈëE[ƒàÑ~ŒA*>9ïÿ%­–HƒìLZÁPJ€"Yø¤ùÛù3îøå̍3¦­ýªïÿ„$©XÚ?4¦­¶™ eœ3¢d"ò?þÿ¯sû{îØm¿ʫ°6ݎóæ´÷ü5¦cąE Ñd¡1³z®ûzL«ã’eÁ0“õäŀ&h$‘t|ʱ2w®¨٩¬º./ߪËõ§tõüÑÚ:··B!ÙPUvv]nöÊQOHäɁÇçÙ=ƒý#H“‘ˆ¦̧Ïl"{YûcaèFÁÙճÓ·;Í'3„ò„³ôÇf7dà@ɳs&%$׆ËJ‚`ñŠãç²Èyô‚ÚBšºôP˜͹3Z¢ÁAâf?eMȫƲÁf;Áóä')-¸¤2DØMÍ
ŸÄ"V&àԪ FCÁJ?
;¾>Ë4³9;—I¹6oÀô7Uéî²ä+Ú֛æÓA	 ¼ž›jbkùãà?¼ÌéǏ%™ÑIEND®B`‚PK
!<ۺ¯''7chrome/content/third-party/cc-logo-cartebancaire@2x.png‰PNG


IHDRH<J<kÇtRNSn¦‘ÜIDATxìIDAÀЌ€Íےé$.ß®ˆ}<Þ\×u]óùõý[Ƭ6í:ƒ­8lŠH)ŒcåH\¶Y¡C	lک‘q©©@ÑijuԸØ?Ö):v¢G'駅ÿ˜&#¹B?™ÞnKÈ*J+é0‹2ú
÷†#¯„޵ÖÇox=ÊD£çM€’€|]õ	Ñ	<PÝvY…;·AÎ
9JˆeQM’P|ûŽH£y¦½YRääçɺּ~½c‡ef´A᪹
ӵ8mÊÛnw8ïĀ%HY
{òÊF’Q(A@}l”H]2{qj¤œ™™Y&g‚Ϛßç?ðr¬ oUZ5‘òEšå¸	Ä&„úMRm'IÌ×DÒ:7*‚6|à߼øøa!'Ÿsk^ß"ŒîMoe0±ˆµkIâ;_3ÅÖãœaGð¹ý™<¤0\ߤ™ØèÈ"D`TÕß5¯¯F€¼‘â>¶ÍlA®c§ŠŽŠ<j¤òs´*&f¥wkR¿Qîʦ†,_íÈ×ÿêæÌòÇ×>f”ð
޵‘z­«	oèi͞tD­Ñш •’í)ÇLl&’øZ3bõ°WૐRÜ©Á^-½¾Åï\±…‰	¤Œ Š]e|d°4t¼‚ëÛçòKˆî0&]®٥zü`"=b$޳qLϟ¿¶Ó-º,Fßáü~?ÿvT'ÛÊíãdú[ärœ+Úôa˯´¡{üaN_²;Ïì?-æ%ËÎEὓt÷Øsýl۶m۶…g۶mëêsm޻hzººrþµ:U=ÏêšoPIYÉNÎÙuĊµ:ȟ–BUüq)Rˆ£c­ˆ‰_øſ„a¿LÊ÷gðl,f–©«ZypͲ
5ÃkÊkˌ³¾]ÐÕ;'Ù=©=9¡3ݙ쁌*æô`Ë ° ¬‘ ºüN¸Ã!(®\,-¢t…†ÏË{¹¼V*ÓòÜòlèáF…ý©Êï?Mnj±ðdɆʃVYbÏU_up}E<†?¡=Ýýåôö§FNob[¶/˜’BlSBéçŽÛf©¦:A0âæùeâøE÷—ñò~²«oÂìÎ7¿üɏ³•;u'*íd o|FD ¤u+óƒ€ló~mœ½ÑJ'­¿BsUþ1ßÎì¸ö½1oµ̃Ãû_*4[.ÝwéAõ(ûä‡cOàÃîlÎ(+sÇ€…X0Œcý›G—ª­g×Rÿ®;|0þ%.5äµãw¸õãÿ]ú樬µ.$¸âY‹Ò Õaۭë#n}Ës©‚¡c-<‰›+^$æ¥ ’Ðùۜ¿ɈÆØfhMJàù‘“zú‹_@ÀJ¹R£/ÜkÅ!ˆ9ø¦Wžù|’‰wî¬S
b?4·.ºL#Öæ—k¨yjß-ݪJaÿuWXЛ;åùoDˆ¨à‘[¯þÜ7ӬR0b‚€úEºð$Œ·î¼þ��7]åÓiíϏš­©áÊVZ¬¹¶º|QOÎ9™ •‹0˜b̂‹·ž·×*Kí¼ܒˆR]¹ó:ïMž—êó„J‰XyY|QÖ‚ÎeƒB€<Px††;ק®¿*@DÇJƒ÷Zc‰G¿êŒRT${²™œ­áÞ(”@«L(”EÑØZ»Ɛæõ‡
AÔ¸ֲý0D„|7½=“õœprES…øå$l~‹ŇčAԬ;¢yh}傞"ÒÌËû÷}<šPÊÉÐ99ë2œŒ ]oØ`
U+ªý|f'¢A®zõëϧ´3n`'VÅw>¶ÀÔC@­¹Dm
Ë6Õ~6£­Ìh”Ƽ…™ëßúážÏ& ̸xE%aÇy!aƒ–0ª&‘À?C€ÙÉÔۓgƒ
tNÏAZ)öÜ9W,d
šŒš:¤ª"p®òKs@‘bߊ]ؓki[ðՔö¶…=ˆǁÐG	­(Š›Ç7 €¢A
 X.ÿ…“æ§NyíS(…°6~˜\£µ1W}8
¾E€
ìwъúGvݧò‰˜ӊXˆVP´	÷Â/–Ȭ¨ܿñrëoþâÄ}†Š…D‘»¿÷LˬgÞjñºj	µDؒߚz7Èâ`Î÷çwõŽo_ôָ9#gýCQ„DA(–‚`RâækG/n›I‹/9dã‹a8æ•Ï3uƙû.ÕP‡ÈzÞ=Ÿµ\üöè¬ﮀ¢¥rÑUÆüà“>`	kmߌdÃÔdÊhæ|‹Ò(‹ÅÎÚvíÛöÞPCM:éTáÓՂħøN2Ðä!þ÷ms1tdº&ÍO+èòØ񛮲Ûj‹‹u%`Ej*åRWQ1	>)Ðê󹳳ž‡¨ùf^çϽ½Z‘Ác6Zކ¢•(ëßÐʀa¥ÞHhjŽ›ßñռYÛ,µ"å©	Ӡ‚r"bµ¡
µUñd¯@b¿¾¬\
p,¼lâCnõµÑ1²µãémˆ)¡ :*ñòDŠԊ†ÐʉæCX%ÜC›wfNyvҏ¯¼¦‹×%’óýK¾Ùã{К
 ¢"Ý××ãYh-.¤„àò˜Ã"@ ÄZ9ï“÷Wk¼ZóÐҗvýW£ޟن˜Üđ1¶ma*—‡Ž¤¥8…óŠ¢@Ÿ,L®Ù֛>ðç_Øý›JòķŽ}í÷caL±P"òàèY ¦3ŠÝx¨óàÔo© BM=aáü]^zò®mwÝy™ðïÉôe¯þꇛÇL°F…¦$œ¾Š›së×ߛÑɄ¡%œ&¤ð0J|…Eû¬Ûՠ0nULÍîNïýú3G¬²æ™ël¼Rã ~©ޜ1ãúïFêX£I}·•$Z£¤³}7}?ñú/&ۘœÐæ©0ò:ß/°(Z>—cŒYáý-£_˜6i§%—ÝsٕÖ2|XeuLë?’¨oZ*õÑܹ/N™þ}çOBń	c’•N%o͜=¼¦ÚþÒÂ#ôtE„ý–Sà°Ö&{s-óSïÏøiÂüŒ(6ð¹BR蒸ýPĐ®>&štßhˆhâù`SEÕÒ5µK×6­ªª—ê^ߟߛ›ו™žJÍÊtg=JkÆXšÿ·gÿ4d=ç<o•ZFùPéK[hQm·ÿÜdü$­ Þã玏Ì3^îý­1@ð&€ÿsؼ‹É`’$¾|Ý=;ØØüé—ƒ/8=9PNzö¥AÀ8ïa?ÿ¹ØÃ‡²Ãèð¾<DausŽ{ß>àGLà-¤0Ø6áÍ6Û@¹
‹`»Ÿ~ù³ f©R?T¦M«¯¼ë¾ί¯®û÷“;Ï+TTBñ‡F…*8E§?L8$_L“$EÕðÒÊ$ï׿üIWM*Ք|"ÎTµ¤³A(y«#UQRy#ªŠãPrþ³ŒDV€*MµÎ_úÃüPáÇw9P&È<üj"Â`8”çq2á1Áñ.Iáe›À·@Ø`ú¸à3Œ×&`7æ38“ïê~ûËÏQ‘¨I¡¥†’*ªš•†ò¦Ïw+J¤Tšª4EɉŸŽ 8€D?	E" ×?f˜‰Q¼³¼ßüåçË|!¼RÌJÕG4ÃÔ2ÉL]¢gȉ,ôAu@EÑåD‡£à'„àÐTƒÿº_%æ‘êýî¯81“³TµLEя§EëÊ•ÂLrYÐtù€i[†(:AQÄÅR^Oå•D)RO]VzçýþoLÃÓÓ,TÞâٵ	dÕꃞÒàT3½/cÉZ˜šœN¤álÎÅ@TÝ%$>
â›P€¢¯:‹ÿc¾ùæ›o¾ùæ_ԁ·°Â¯BIEND®B`‚PK
!<øu+-chrome/content/third-party/cc-logo-diners.svg<svg width="36" height="30" xmlns="http://www.w3.org/2000/svg"><g fill-rule="nonzero" fill="none"><path d="M19.863 20.068c4.698.022 8.987-3.839 8.987-8.536 0-5.137-4.289-8.688-8.987-8.686h-4.044c-4.755-.002-8.669 3.55-8.669 8.686 0 4.698 3.914 8.559 8.669 8.536h4.044z" fill="#4186CD"/><path d="M15.76 3.535a7.923 7.923 0 0 0 0 15.844 7.923 7.923 0 0 0 0-15.844zm-4.821 7.75c.004-2.122 1.288-3.931 3.1-4.65v9.3c-1.812-.719-3.096-2.527-3.1-4.65zm6.544 4.65v-9.3c1.811.717 3.097 2.527 3.1 4.65-.003 2.123-1.289 3.931-3.1 4.65z" fill="#FFF"/><g fill="#211E1F"><path d="M.65 22.925c0-.71-.375-.663-.733-.671v-.205c.31.015.63.015.94.015.336 0 .79-.015 1.381-.015 2.065 0 3.19 1.365 3.19 2.763 0 .782-.462 2.748-3.286 2.748-.407 0-.782-.016-1.157-.016-.358 0-.71.008-1.068.016v-.205c.478-.048.71-.064.733-.6v-3.83zm.644 3.636c0 .586.437.654.825.654 1.713 0 2.275-1.24 2.275-2.373 0-1.422-.951-2.449-2.48-2.449-.326 0-.476.022-.62.03v4.138zM5.428 27.364h.152c.225 0 .387 0 .387-.25v-2.041c0-.332-.121-.378-.419-.528v-.12c.378-.107.83-.249.861-.272a.301.301 0 0 1 .145-.038c.04 0 .057.046.057.106v2.893c0 .25.177.25.402.25h.137v.196c-.274 0-.556-.015-.845-.015-.29 0-.58.007-.877.015v-.196zm.689-4.627a.36.36 0 0 1-.345-.35c0-.177.169-.338.345-.338.182 0 .344.148.344.337 0 .19-.155.351-.344.351zM7.993 25.117c0-.278-.084-.353-.438-.496v-.143c.325-.106.634-.204.996-.363.022 0 .045.016.045.076v.49c.43-.309.8-.566 1.307-.566.64 0 .867.468.867 1.055v1.944c0 .25.166.25.377.25h.136v.196c-.265 0-.528-.015-.8-.015s-.544.007-.815.015v-.196h.136c.211 0 .362 0 .362-.25v-1.95c0-.43-.263-.642-.694-.642-.241 0-.626.196-.876.362v2.23c0 .25.166.25.378.25h.136v.196c-.264 0-.529-.015-.8-.015-.272 0-.544.007-.816.015v-.196h.137c.21 0 .362 0 .362-.25v-1.997zM11.943 25.569c-.017.072-.017.192 0 .465.049.762.553 1.388 1.212 1.388.453 0 .809-.24 1.113-.537l.115.113c-.38.489-.849.906-1.525.906-1.31 0-1.575-1.236-1.575-1.75 0-1.573 1.089-2.039 1.665-2.039.668 0 1.386.41 1.394 1.26 0 .05 0 .097-.008.145l-.074.049h-2.317zm1.514-.42c.212 0 .237-.077.237-.147 0-.3-.264-.542-.742-.542-.52 0-.877.264-.98.689h1.485zM14.383 27.364h.191c.198 0 .34 0 .34-.25v-2.117c0-.233-.262-.279-.368-.339v-.113c.516-.234.799-.43.863-.43.042 0 .063.023.063.099v.678h.015c.176-.294.474-.777.905-.777.176 0 .402.128.402.4 0 .203-.133.385-.331.385-.22 0-.22-.182-.468-.182-.12 0-.516.174-.516.626v1.77c0 .25.142.25.34.25h.395v.196c-.389-.008-.684-.015-.99-.015-.289 0-.586.007-.84.015v-.196zM17.282 26.668c.102.53.418.98.996.98.465 0 .64-.29.64-.57 0-.948-1.724-.643-1.724-1.935 0-.45.357-1.028 1.226-1.028.252 0 .592.073.9.234l.056.818h-.182c-.079-.505-.355-.795-.862-.795-.316 0-.616.185-.616.53 0 .94 1.834.65 1.834 1.91 0 .53-.42 1.092-1.36 1.092a2.06 2.06 0 0 1-.964-.272l-.087-.924.143-.04zM26.431 23.625h-.192c-.147-.94-.786-1.318-1.649-1.318-.886 0-2.173.618-2.173 2.548 0 1.626 1.11 2.792 2.296 2.792.763 0 1.395-.547 1.55-1.392l.176.048-.177 1.175c-.323.21-1.194.426-1.703.426-1.802 0-2.942-1.214-2.942-3.024 0-1.649 1.41-2.831 2.92-2.831.623 0 1.224.21 1.817.427l.077 1.15zM26.783 27.36h.153c.226 0 .387 0 .387-.253v-4.268c0-.498-.12-.514-.427-.598v-.123c.322-.099.66-.237.83-.33.087-.045.152-.084.176-.084.05 0 .065.046.065.108v5.295c0 .254.177.254.403.254h.136v.199c-.273 0-.555-.016-.845-.016-.29 0-.58.008-.878.016v-.2zM31.775 27.032c0 .136.084.143.214.143.092 0 .206-.007.305-.007v.159c-.328.03-.955.188-1.1.233l-.038-.023v-.61c-.458.37-.81.633-1.353.633-.412 0-.84-.264-.84-.897v-1.93c0-.196-.03-.384-.457-.422v-.143c.275-.007.885-.053.984-.053.085 0 .085.053.085.219v1.944c0 .226 0 .874.664.874.26 0 .604-.195.924-.458v-2.029c0-.15-.366-.233-.64-.309v-.135c.686-.046 1.115-.106 1.19-.106.062 0 .062.053.062.136v2.78zM33.372 24.72c.323-.27.76-.572 1.206-.572.94 0 1.505.804 1.505 1.671 0 1.042-.776 2.085-1.935 2.085-.599 0-.914-.191-1.125-.278l-.243.182-.169-.087a9.26 9.26 0 0 0 .113-1.416v-3.422c0-.518-.122-.534-.43-.621v-.128c.325-.103.664-.246.834-.342.09-.048.154-.088.18-.088.047 0 .064.048.064.112v2.905zm-.044 2.032c0 .301.291.808.834.808.868 0 1.232-.831 1.232-1.535 0-.854-.664-1.565-1.296-1.565-.3 0-.552.19-.77.372v1.92z"/></g></g></svg>PK
!<Ȋ+]]/chrome/content/third-party/cc-logo-discover.png‰PNG


IHDR%«#×m$IDATxbÒ`ÈÉÉõGDDâýԑ‘‘û.y{{?âûÄb//¯Çêêêy@CÿD ½½½žä³Á䨶¶¶:°£YHÕ:GÕÕյ˜+Ç8Yî Šækœ<{fžm۶mÛæÌ<۶m۶m¯w»Ç}Ò'kï¶Oݺ¿ú'8¨îݻ¯LØP+W®dܸq¬X±üüüX¶l9Š¢°téR̕+W]»vþýû3pà@>|ü„ÝχG«fópê@¾ہח¯Ì_°€   Dׯ_góæÍl߾qãÌX,nܸT)S°Z­\¾|™{÷*=zôXù>SNjšFŽ9èڵ+7¦\¹r\¼x‘\¹r±@¿y¶lÙX¼x1{÷îåìٳdȐ)`äȑœ>~Œë½ëó°j"Þ4LλfÉñÞl!o‚lܸQ͚5™4iժU£U«VÈ=wïÞMš4i0›͘L&.]º„Ëå§þ*X° B/*S¦âD‰%X»v-ƒٳgHíڵ™<y2é͑<¨‘”/Œ|íjħo:\Ó3oÒ(j֪Íóç/Ȝ93_¿~EŽ´mۖ…rèÐ!òæ͋¸/P<%'ˆêׯO§N(T¨v»óçÏS«V-¨téÒè_út`ïš'珿aF­ló²ñåÙUòå/ˆ>Ùé֭¢*Uª Dž©S§røða’&MJ¢D‰˜9sfðL	TîܹY¾|9óæÍ#k֬ì۷<yò°zõjä¿üJe³fÍBΕ6ž:uš;gŽá;¦ £R LM‹:;	΃­ⶾ’qãæMDɑ¸rìØ1Ĉ±cÇJ±’Ý`P0±Wz.ÁäӧOr¢bŊ”,YRr€œ+•‰cÒæÛwïÃû«ط5q©4®Ó}ЂÞ#’8ˆãrH2(ϐNH>ûöí+0´oߞ£G‚@õèñ‘àñx‚mËÃþ#—JT¥É=Ÿ¢Kõ¼	cNi^Ïpìê‡Íj¤SïâJó{‡óèÔ)¹PF'ÇfIG§"?Å”æýâ˜iùPF%G—ÅlСq¥ixÞ\űg êäœÿ‘OœBiŸp]߀}Uc‹IÚ&N 4Õ÷£Ã^Ôét(cR¢˜Óý7PšÿÜ÷öâØÝuV1”±©Cr%v¡4Åϫ‹8ÏΞ®%êÔ<(cRý	’&Ø#%kßi“3¼ž¢=:ˆûäTœZá˜U»E¿јäØƦÄfN'ÛÑú8­:Ôo3ã[È %%µؽyˆ³ëãéqßËJ��Ð}eæûŠ@š龇û}OuN æ¢öPãz$+ÈÒÍ0”Á([#0å"±¶QIEND®B`‚PK
!<àvá§	§	2chrome/content/third-party/cc-logo-discover@2x.png‰PNG


IHDRJ<k,­	nIDATxí›pd[…3Æcì¤ólÛ۶m#øcŒmÛ5¶’ŒÍd’ô ¶Úé^ÿYՓ[AϳsWծ\|gï}v’n+Y²dɒ%«â¨råÊÓíììN8::û7˜ƒƒÃ)1Þx[[[å_aÖÖ֏£¡V¢ÓS—._FRR’lŒl<<<¼­¸J©©©eYdóâ‹/Nµ"(’“eYdóß%ƒ’AɠdP2¨)2¨_ÊÝÝ}žê€ruu]!ƒúe –ˠdP"(­V‹;wîàöí۠ݻ‡üü|”UFFx¿äïˆ8qâöî݋»wc`Iz½çϟÇúõë±e˰/£ш’Rçd"áø^Ü\·1;Ö ãîu˜JôqóæMÄÄĔ{ÊÍÍÇ•JÅãrƹòݘØXéZ¬8æ»źuëÒÒҞŠƒxúé§Q­ZuXYY¡J•*ðT(0hÐ $$$ Xþþþ¨Q£F
êҥKx÷ÝwÁwhU«VEûö푙™‰b9rŸþ9ïKV³fM¬Y³ź¹yv6xû?zQ_Zãü·68×À7½{C’ˆÄä8;;£víڈŒŒDY
0իWÇðáÃqùòeŽQ̥Z©>»uë†ÂÂB¸¹¹ñY麧§'|}}ÁÅðööƾ}ûžêƍ ¤Y³føþûïñÜóÏK
]¼x”xmĈ(**ÂG}žóù   üøãøä“O““jûöí¨U«øÌ|€aÆ¡o߾[‚@€öâ0ìx÷\¯猇íHîª@J7R»{"±½ý(ÌFÏÞ}Àvø~I¥¤¤@üÉ?ŽcåsÏ<óZ´h–-[‚6{ölzôloÑ^»ví$h3fÌÀÇ‘ýӠ*Uª„^xƒ”R©D£FÀFÞ{ï=èt:øùùçcƌAbb"=ˆžÈP,1ɛØ6½€Ϗ7Ŋ‹‹Cô™3H¿y	;>±ÇÝ&î  d(¥»€ÔS´>
dP ³Ÿô[Æ#2ú,'HȥB޼y`ß~û-¨èèhðüwÞAY¥§§Bj#00|þ»テ¤ŸåååE÷,ù7>6tøða„„„€ǣFÉsÕøÞàC‹’š>}:øìgŸ}·¤Hß!8÷µ
!™õ€z@ý Á
dP g¬'òý^‡!K‰/¾úlsѢE Ø~
^[½z5¨¨¨(ðüõ×_gN¥wӸP<—@Ï}åʕàóß|óÍoE1¶ÙC+,<<f. Û|×èY:tÀիWA1Wñ:W̒ŠŠ8Öå;ķt1{P/Òû
@ƒ á^È£@î$ò}„ù9NbɊ5`›_~ù%(æ«ʕ+3=0¬J¢·ÛÛۃH‹ˆˆàÜÄ5;0Gn޼ë֭ÃË/¿,ó÷€Þ2lˆù),,¬(êàC`¬ӻx+výúu´iÓ<Ÿ5k,I¯Ó!²˗Hêäf4@ª@ö(/äNP oªä¯@a¨°0'˜bw!3'gB`ôdö1yòdP%Aƛo¾‰·Þz´0aZ©$omm¡Æq޿·SɵIß(IwïÞ9ïO˜0“&M{ôè'éʘvHí挬!Ðhœ4™¤@A°ªԳ„Íó€1ù<(öÍv[µjÅŒaľˁ"$†7çÂE/{öÙgéýÒFTN¿$™³Ñb͟?Ÿ×é%LÒ,J:yòdɺ†۫´3;wŽïrPbðÑåkŸ¼<$î^…¤È논‰æ0+. Í€æ* ™ï
Ýֺ€Aêò•+Üþ%hݺ5¨ ~6™3M°†bóYÎëƒb¬‹k¢ ܊U«V¡k׮œh©äɜÄóñãÇcë֭à.Կ°˜ܿ?ÿ,Q*ÜèM<§»‡‹Áç8¾3Kl×&iá͐?ÁÃL5]š#-ð‚v±tKÝ`TîA±L&ê֫Çv9>ökóÖΝ;±{÷nЮÀYYYR2ONNf9Ác‚çfõó ®]»ÆÆËóA1$й€×Y±H¥–{‡»D±ºï޽»ŶCCCAe=€zEs¨B žáÍ\7hºA·ØºÕ/¡èör”Õƍ%¯áo%uúôi‹ýµmۖ9Š`x^\HK%““˖ŸÅ2€Ézʔ) Ñùëï—ԱcÇÀûüu…bFxuëÖEýúõÁ÷¸—աC‡ÀÄ[OxB“&M>î߿IúB®­„nwgè·þý®F0DO€)ã,‰аaC¬X±eõàÁޱœm۶
¬ƒ‚‚	G*Šù+˞‰'‚¿EH RX¹ÿƒ)6ê“·ï\€«½ÍAÉ*ÒÁxnâ½߂Ë3•eP)£¡YÞ
Z‘¯ƸÀå¹j2¨’2e&@·k4Tþ
¨¼¡
ðDÜXw”¨ úcaP‡½
ÕT¨ü< úŸ§J¤ʆ>r>Ô3>yȝ€h2(3 ,è£B=ë3 _·’€dP¦¼dèO͂zæg£äZŒʘÝA?¨#Þ/çA2(cŠNC»uTA¯>ÎA w0ÅÕÐ,knNÎގÒ.F«ؠŒœ…nÏD¨§}h†ããl@Eeʌ‡>j4Kšpbåë"ƒâÎe¸¼Úuݠ~ÍÇÇELL
¯Šʔ›ÃÕ-Ðnêuø;-aNårOÅe2˜q†ó« ]ߝ¿Z­\õ\á@™4¹(Jˆ‚þh¨yÇ
z…E¡%8”^
cò
á5+ ÝÜꙟˆ”sʅUEeÒäXxE€Y	ݶ!ÐÌý*ÿ†9Çrµüõç|Åèr”@ì!OLƒaCwèf.:V@ëãh6?7hý9˜®‚<¡÷¿Yõ{¿4”wÎíƃÝP.덄Ðo7éečrDÜ[čt@Üh±:n\¡)ǻ#j +ž©¶’_C[ommýà÷|UËæÙZJ›êVJ›jÂø³¦°Z•”6µ«üëͺV凕+[…	PV²ý“!ȠdP2(ÔÀþe¿¶3^ÚuIEND®B`‚PK
!<7+~«¦¦*chrome/content/third-party/cc-logo-jcb.svg<svg width="30" height="30" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path d="M3.622.575C1.734.575.009 2.278.009 4.188c0 1.051 0 5.212-.002 9.348.346.217 2.01.752 2.68.799 1.466.103 2.375-.381 2.515-1.603l-.007-4.538h3.243v4.434c-.17 2.54-2.399 3.057-5.79 2.887-.877-.046-2.07-.27-2.64-.42L.004 22.94H5.65c1.54 0 3.544-1.439 3.544-3.627V.575H3.622z" id="a"/><linearGradient x1="-.003%" y1="49.999%" x2="100.002%" y2="49.999%" id="b"><stop stop-color="#313477" offset="0%"/><stop stop-color="#0077BC" offset="100%"/></linearGradient><path d="M0 1.564l.007.001V.007L0 .002v1.562z" id="d"/><linearGradient x1="0%" y1="50.019%" x2="1.21%" y2="50.019%" id="e"><stop stop-color="#313477" offset="0%"/><stop stop-color="#0077BC" offset="100%"/></linearGradient><path d="M3.976.575C2.088.575.363 2.278.363 4.188v4.945c1.132-.885 2.958-1.319 5.281-1.14 1.322.102 2.3.286 2.834.445v1.57c-.588-.294-1.748-.73-2.715-.8-2.191-.158-3.342.868-3.342 2.528 0 1.494.888 2.773 3.331 2.602.806-.056 2.148-.525 2.719-.797l.007 1.523c-.492.155-2.02.488-3.458.5-2.165.017-3.694-.443-4.659-1.189L.36 22.941h5.643c1.54 0 3.546-1.439 3.546-3.627V.575H3.976z" id="g"/><linearGradient x1=".004%" y1="49.999%" x2="99.996%" y2="49.999%" id="h"><stop stop-color="#753136" offset="0%"/><stop stop-color="#ED1746" offset="100%"/></linearGradient><path d="M.123.448L.119 2.424l2.21.007c.43 0 .97-.368.97-1.01a.97.97 0 0 0-.967-.973c-.308.003-.8.001-1.245 0L.375.446C.26.446.17.446.123.448" id="j"/><linearGradient x1="0%" y1="50.008%" x2="99.996%" y2="50.008%" id="k"><stop stop-color="#008049" offset="0%"/><stop stop-color="#62BA44" offset="100%"/></linearGradient><path d="M.115.473l-.008 1.8 2.089.014c.346-.007.834-.325.834-.882 0-.567-.426-.95-.88-.939-.296.008-.702.005-1.078.002L.52.465C.333.465.187.467.115.473" id="m"/><linearGradient x1=".022%" y1="49.994%" x2="100.012%" y2="49.994%" id="n"><stop stop-color="#008049" offset="0%"/><stop stop-color="#62BA44" offset="100%"/></linearGradient><path d="M3.694.575C1.806.575.08 2.278.08 4.188L.08 8.164h5.365c1.067 0 2.324.457 2.324 1.754 0 .696-.37 1.485-1.706 1.74v.03c.78 0 2.132.457 2.132 1.833 0 1.423-1.46 1.817-2.243 1.817l-5.873.006-.001 7.597H5.72c1.54 0 3.544-1.439 3.544-3.627V.575H3.694z" id="p"/><linearGradient x1="-.007%" y1="49.999%" x2="100.004%" y2="49.999%" id="q"><stop stop-color="#008049" offset="0%"/><stop stop-color="#62BA44" offset="100%"/></linearGradient></defs><g fill="none" fill-rule="evenodd"><g transform="translate(0 3.013)"><mask id="c" fill="#fff"><use xlink:href="#a"/></mask><path d="M3.622.575C1.734.575.009 2.278.009 4.188c0 1.051 0 5.212-.002 9.348.346.217 2.01.752 2.68.799 1.466.103 2.375-.381 2.515-1.603l-.007-4.538h3.243v4.434c-.17 2.54-2.399 3.057-5.79 2.887-.877-.046-2.07-.27-2.64-.42L.004 22.94H5.65c1.54 0 3.544-1.439 3.544-3.627V.575H3.622z" fill="url(#b)" mask="url(#c)"/></g><g transform="translate(0 16.543)"><mask id="f" fill="#fff"><use xlink:href="#d"/></mask><path d="M0 1.564l.007.001V.007L0 .002v1.562z" fill="url(#e)" mask="url(#f)"/></g><g transform="translate(10 3.013)"><mask id="i" fill="#fff"><use xlink:href="#g"/></mask><path d="M3.976.575C2.088.575.363 2.278.363 4.188v4.945c1.132-.885 2.958-1.319 5.281-1.14 1.322.102 2.3.286 2.834.445v1.57c-.588-.294-1.748-.73-2.715-.8-2.191-.158-3.342.868-3.342 2.528 0 1.494.888 2.773 3.331 2.602.806-.056 2.148-.525 2.719-.797l.007 1.523c-.492.155-2.02.488-3.458.5-2.165.017-3.694-.443-4.659-1.189L.36 22.941h5.643c1.54 0 3.546-1.439 3.546-3.627V.575H3.976z" fill="url(#h)" mask="url(#i)"/></g><g transform="translate(22.353 14.778)"><mask id="l" fill="#fff"><use xlink:href="#j"/></mask><path d="M.123.448L.119 2.424l2.21.007c.43 0 .97-.368.97-1.01a.97.97 0 0 0-.967-.973c-.308.003-.8.001-1.245 0L.375.446C.26.446.17.446.123.448" fill="url(#k)" mask="url(#l)"/></g><g transform="translate(22.353 11.837)"><mask id="o" fill="#fff"><use xlink:href="#m"/></mask><path d="M.115.473l-.008 1.8 2.089.014c.346-.007.834-.325.834-.882 0-.567-.426-.95-.88-.939-.296.008-.702.005-1.078.002L.52.465C.333.465.187.467.115.473" fill="url(#n)" mask="url(#o)"/></g><g transform="translate(20.588 3.013)"><mask id="r" fill="#fff"><use xlink:href="#p"/></mask><path d="M3.694.575C1.806.575.08 2.278.08 4.188L.08 8.164h5.365c1.067 0 2.324.457 2.324 1.754 0 .696-.37 1.485-1.706 1.74v.03c.78 0 2.132.457 2.132 1.833 0 1.423-1.46 1.817-2.243 1.817l-5.873.006-.001 7.597H5.72c1.54 0 3.544-1.439 3.544-3.627V.575H3.694z" fill="url(#q)" mask="url(#r)"/></g></g></svg>PK
!<Â^AEH
H
1chrome/content/third-party/cc-logo-mastercard.svg<svg width="38" height="30" xmlns="http://www.w3.org/2000/svg"><g fill-rule="nonzero" fill="none"><path d="M7.485 29.258v-1.896a1.125 1.125 0 0 0-1.188-1.2 1.17 1.17 0 0 0-1.061.537 1.109 1.109 0 0 0-.999-.537.998.998 0 0 0-.885.448v-.373h-.657v3.021h.664v-1.662a.708.708 0 0 1 .74-.802c.435 0 .656.284.656.796v1.68h.664v-1.674a.71.71 0 0 1 .74-.802c.448 0 .663.284.663.796v1.68l.663-.012zm9.817-3.02h-1.08v-.917h-.664v.916h-.6v.6h.613v1.391c0 .701.271 1.119 1.049 1.119.29 0 .575-.08.821-.234l-.19-.563a1.213 1.213 0 0 1-.58.17c-.317 0-.437-.201-.437-.505v-1.377h1.074l-.006-.6zm5.605-.076a.891.891 0 0 0-.796.442v-.367h-.65v3.021h.656v-1.693c0-.5.215-.778.632-.778.14-.002.28.024.411.076l.202-.632a1.406 1.406 0 0 0-.467-.082l.012.013zm-8.474.316a2.26 2.26 0 0 0-1.232-.316c-.765 0-1.264.366-1.264.966 0 .493.367.797 1.043.891l.316.045c.36.05.53.145.53.316 0 .234-.24.366-.688.366-.361.01-.715-.1-1.005-.316l-.316.512a2.18 2.18 0 0 0 1.308.392c.872 0 1.378-.41 1.378-.986 0-.575-.398-.809-1.056-.904l-.316-.044c-.284-.038-.511-.095-.511-.297 0-.202.214-.354.575-.354.333.004.659.093.947.26l.291-.531zm17.602-.316a.891.891 0 0 0-.796.442v-.367h-.65v3.021h.656v-1.693c0-.5.215-.778.632-.778.14-.002.28.024.411.076l.202-.632a1.406 1.406 0 0 0-.467-.082l.012.013zm-8.467 1.58a1.526 1.526 0 0 0 1.611 1.58 1.58 1.58 0 0 0 1.087-.36l-.316-.532a1.327 1.327 0 0 1-.79.272.97.97 0 0 1 0-1.934c.286.003.563.099.79.272l.316-.53a1.58 1.58 0 0 0-1.087-.361 1.526 1.526 0 0 0-1.611 1.58v.012zm6.155 0v-1.505h-.658v.367a1.147 1.147 0 0 0-.948-.442 1.58 1.58 0 0 0 0 3.16c.37.013.722-.152.948-.443v.366h.658v-1.504zm-2.446 0a.913.913 0 1 1 .916.966.907.907 0 0 1-.916-.967zm-7.93-1.58a1.58 1.58 0 1 0 .044 3.16c.454.023.901-.124 1.254-.411l-.316-.487c-.25.2-.559.311-.878.316a.837.837 0 0 1-.904-.74h2.243v-.252c0-.948-.587-1.58-1.434-1.58l-.01-.006zm0 .587a.749.749 0 0 1 .764.733h-1.58a.777.777 0 0 1 .803-.733h.012zm16.464.999v-2.724h-.632v1.58a1.147 1.147 0 0 0-.948-.442 1.58 1.58 0 0 0 0 3.16c.369.013.722-.152.948-.443v.366h.632v-1.497zm1.096 1.07a.316.316 0 0 1 .218.086.294.294 0 0 1-.098.487.297.297 0 0 1-.12.025.316.316 0 0 1-.284-.183.297.297 0 0 1 .066-.329.316.316 0 0 1 .228-.085h-.01zm0 .535a.224.224 0 0 0 .165-.07.234.234 0 0 0 0-.316.234.234 0 0 0-.165-.07.237.237 0 0 0-.167.07.234.234 0 0 0 0 .316.234.234 0 0 0 .076.05c.032.015.066.021.101.02h-.01zm.02-.376a.126.126 0 0 1 .082.025c.02.016.03.041.028.066a.076.076 0 0 1-.022.057.11.11 0 0 1-.066.029l.091.104h-.072l-.086-.104h-.028v.104h-.06v-.278l.132-.003zm-.07.054v.075h.07a.066.066 0 0 0 .037 0 .032.032 0 0 0 0-.028.032.032 0 0 0 0-.028.066.066 0 0 0-.038 0l-.07-.02zm-3.476-1.283a.913.913 0 1 1 .917.967.907.907 0 0 1-.917-.967zm-22.19 0v-1.51h-.657v.366a1.147 1.147 0 0 0-.948-.442 1.58 1.58 0 1 0 0 3.16c.369.013.722-.152.948-.443v.366h.657v-1.497zm-2.445 0a.913.913 0 1 1 .916.967.907.907 0 0 1-.922-.967h.006z" fill="#231F20"/><path fill="#FF5F00" d="M14.215 3.22h9.953v17.886h-9.953z"/><path d="M14.847 12.165a11.356 11.356 0 0 1 4.345-8.945 11.375 11.375 0 1 0 0 17.886 11.356 11.356 0 0 1-4.345-8.941z" fill="#EB001B"/><path d="M37.596 12.165a11.375 11.375 0 0 1-18.404 8.941 11.375 11.375 0 0 0 0-17.886 11.375 11.375 0 0 1 18.404 8.941v.004zM36.51 19.265v-.412h.148v-.085h-.376v.085h.161v.412h.066zm.73 0v-.497h-.115l-.132.355-.133-.355h-.101v.497h.082v-.373l.123.323h.086l.123-.323v.376l.066-.003z" fill="#F79E1B"/></g></svg>PK
!<±x“ÎÎ*chrome/content/third-party/cc-logo-mir.svg<svg width="36" height="30" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient x1="100%" y1="312.751%" x2=".612%" y2="312.751%" id="a"><stop stop-color="#1E5CD8" offset="0%"/><stop stop-color="#02AFFF" offset="100%"/></linearGradient></defs><g fill-rule="nonzero" fill="none"><path d="M7.812 11.313l-1.326 4.593h-.227l-1.326-4.594A1.823 1.823 0 0 0 3.18 10H0v10h3.184v-5.91h.227L5.234 20H7.51l1.819-5.91h.226V20h3.185V10H9.56c-.81 0-1.522.535-1.75 1.313zM25.442 20h3.204v-2.957h3.223c1.686 0 3.122-.953 3.677-2.293H25.442V20zm-5.676-8.945l-2.241 4.855h-.227V10h-3.184v10h2.703c.712 0 1.357-.414 1.654-1.055l2.242-4.851h.227V20h3.184V10H21.42c-.712 0-1.358.414-1.655 1.055z" fill="#006848"/><path d="M32.186 0c.92 0 1.752.352 2.382.93a3.49 3.49 0 0 1 1.146 2.59c0 .21-.023.417-.058.62H29.74a4.478 4.478 0 0 1-4.272-3.124c-.007-.02-.011-.043-.02-.067-.015-.054-.027-.113-.042-.168A4.642 4.642 0 0 1 25.293 0h6.893z" fill="url(#a)" transform="translate(0 10)"/></g></svg>PK
!<ÀzÄ"ÆÆ/chrome/content/third-party/cc-logo-unionpay.svg<svg width="36" height="30" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M0 .04h17.771v22.433H0z"/><path id="c" d="M.134.04h18.093v22.433H.134z"/><path id="e" d="M.202.04h17.77v22.433H.202z"/></defs><g fill="none" fill-rule="evenodd"><g transform="translate(0 3.179)"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><path d="M7.023.04h8.952C17.225.04 18 1.057 17.71 2.31l-4.168 17.893c-.294 1.25-1.545 2.269-2.795 2.269h-8.95c-1.248 0-2.027-1.02-1.736-2.269l4.17-17.893C4.52 1.058 5.771.04 7.022.04" fill="#E21837" mask="url(#b)"/></g><g transform="translate(8.073 3.179)"><mask id="d" fill="#fff"><use xlink:href="#c"/></mask><path d="M7.157.04h10.294c1.25 0 .686 1.018.392 2.271l-4.167 17.893c-.292 1.25-.201 2.269-1.453 2.269H1.93c-1.252 0-2.026-1.02-1.732-2.269L4.363 2.311C4.66 1.058 5.907.04 7.157.04" fill="#00457C" mask="url(#d)"/></g><g transform="translate(17.89 3.179)"><mask id="f" fill="#fff"><use xlink:href="#e"/></mask><path d="M7.224.04h8.952c1.251 0 2.028 1.018 1.734 2.271l-4.166 17.893c-.295 1.25-1.547 2.269-2.798 2.269H2c-1.252 0-2.028-1.02-1.735-2.269L4.432 2.311C4.723 1.058 5.972.04 7.224.04" fill="#007B84" mask="url(#f)"/></g><path d="M26.582 16.428L25.49 20.04h.295l-.228.746h-.292l-.069.23h-1.038l.07-.23H22.12l.21-.69h.215l1.106-3.667.22-.739h1.06l-.111.373s.282-.203.55-.272c.266-.07 1.801-.096 1.801-.096l-.227.734h-.362zm-1.866 0l-.28.923s.315-.142.484-.189c.174-.046.434-.061.434-.061l.203-.673h-.841zm-.42 1.38l-.29.96s.321-.163.492-.215c.174-.039.438-.072.438-.072l.205-.673h-.845zm-.675 2.24h.844l.242-.81h-.841l-.245.81z" fill="#FEFEFE"/><path d="M27.05 15.694h1.13l.012.42c-.008.072.054.106.186.106h.23l-.21.695h-.612c-.528.038-.73-.19-.715-.445l-.022-.776zM27.2 18.993H26.12l.185-.619h1.232l.175-.566h-1.216l.207-.698h3.384l-.21.698h-1.135l-.178.566h1.139l-.19.619h-1.229l-.219.26h.5l.121.78c.014.078.014.13.04.162.025.028.175.042.262.042h.152l-.231.759h-.385c-.058 0-.147-.005-.27-.01-.114-.01-.195-.077-.273-.116a.367.367 0 0 1-.202-.265l-.12-.778-.56.766c-.177.243-.417.428-.824.428h-.782l.205-.677h.3a.484.484 0 0 0 .218-.063.336.336 0 0 0 .166-.138l.816-1.15zM15.397 17.298h2.855l-.211.68H16.9l-.179.581h1.168l-.213.702h-1.167l-.284.945c-.034.104.278.117.39.117l.584-.08-.235.778H15.65c-.106 0-.185-.015-.299-.04a.312.312 0 0 1-.209-.153c-.048-.077-.122-.14-.071-.305l.378-1.25H14.8l.215-.714h.65l.173-.581h-.648l.207-.68zM17.317 16.074h1.171l-.212.712h-1.6l-.173.15c-.075.072-.1.042-.198.094-.09.045-.28.136-.525.136h-.513l.207-.684h.154c.13 0 .219-.012.264-.04a.617.617 0 0 0 .171-.222l.296-.535h1.163l-.205.389zM18.991 15.694h.997l-.146.502s.316-.252.536-.343c.22-.081.716-.154.716-.154l1.615-.01-.55 1.832a2.139 2.139 0 0 1-.269.608.7.7 0 0 1-.271.251 1.02 1.02 0 0 1-.375.126c-.106.008-.27.01-.496.014h-1.556l-.437 1.447c-.042.144-.061.213-.034.252a.18.18 0 0 0 .148.073l.686-.065-.235.794h-.766c-.245 0-.422-.006-.547-.015-.118-.01-.242 0-.325-.063-.07-.063-.18-.147-.177-.231.007-.078.04-.209.09-.389l1.396-4.63zm2.117 1.848h-1.634l-.1.33h1.414c.167-.02.202.004.216-.004l.104-.326zm-1.545-.297s.32-.292.867-.387c.124-.023.9-.015.9-.015l.119-.392h-1.647l-.24.794z" fill="#FEFEFE"/><path d="M21.899 18.648l-.093.44c-.04.137-.073.24-.177.328-.11.093-.237.19-.536.19l-.554.023-.005.497c-.005.14.032.126.054.149.026.025.049.035.073.045l.175-.01.529-.03-.22.726h-.606c-.425 0-.74-.01-.842-.091-.103-.065-.116-.146-.115-.286l.04-1.938h.968l-.014.397h.233c.08 0 .134-.008.167-.03a.175.175 0 0 0 .065-.1l.097-.31h.76zM8.082 8.932c-.033.158-.655 3.024-.656 3.026-.134.58-.231.993-.562 1.26a1 1 0 0 1-.66.23c-.409 0-.646-.203-.687-.587l-.007-.132.124-.781s.652-2.611.769-2.957l.01-.039c-1.27.011-1.495 0-1.51-.02-.009.028-.04.19-.04.19l-.666 2.943-.057.25-.11.816c0 .242.047.44.142.607.303.53 1.168.609 1.657.609.63 0 1.222-.134 1.622-.378.694-.41.875-1.051 1.037-1.62l.075-.293s.672-2.712.786-3.065c.004-.02.006-.03.012-.039-.92.01-1.192 0-1.28-.02M11.798 14.319c-.45-.008-.61-.008-1.135.02l-.02-.04c.045-.2.095-.398.14-.6l.065-.275c.097-.425.191-.92.202-1.072.01-.09.042-.317-.218-.317-.109 0-.223.053-.339.107-.063.226-.19.863-.252 1.153-.13.61-.138.681-.197.983l-.038.041a12.946 12.946 0 0 0-1.159.02l-.024-.046c.089-.362.178-.728.263-1.091.224-.986.278-1.362.338-1.863l.044-.03c.52-.073.647-.088 1.21-.202l.048.053-.087.313c.096-.057.187-.114.283-.163.266-.13.562-.17.724-.17.248 0 .518.069.63.355.107.254.036.567-.104 1.184l-.072.316c-.144.686-.168.812-.25 1.283l-.052.041zM13.627 14.319c-.272-.002-.448-.008-.617-.002-.17.002-.335.01-.588.022l-.013-.022-.016-.024c.069-.26.106-.35.14-.443a3.13 3.13 0 0 0 .128-.449c.08-.345.128-.586.16-.797.037-.204.057-.378.085-.58l.02-.015.02-.02c.27-.037.442-.062.618-.09.177-.023.355-.06.635-.113l.01.024.008.025c-.052.214-.105.427-.156.643-.05.217-.103.43-.15.643-.101.453-.142.623-.166.745-.024.115-.03.178-.069.412l-.025.021-.024.02zM17.67 12.768c.159-.692.036-1.015-.119-1.212-.234-.3-.648-.396-1.078-.396-.258 0-.873.025-1.354.468-.345.32-.505.754-.6 1.17-.098.423-.21 1.186.492 1.47.216.093.528.118.73.118.513 0 1.04-.141 1.436-.561.305-.341.445-.848.494-1.057m-1.18-.05c-.022.117-.124.551-.262.736-.097.136-.21.219-.337.219-.037 0-.26 0-.264-.332-.002-.163.031-.33.072-.512.119-.524.258-.964.616-.964.28 0 .3.328.175.853M28.677 14.365c-.544-.004-.7-.004-1.202.017l-.031-.04c.135-.517.272-1.032.393-1.554.158-.678.194-.966.245-1.363l.041-.033c.54-.077.69-.099 1.252-.203l.016.047c-.103.426-.203.85-.304 1.278-.206.893-.281 1.346-.36 1.813l-.05.038z" fill="#FEFEFE"/><path d="M28.935 12.83c.158-.688-.479-.062-.58-.289-.154-.354-.058-1.072-.683-1.312-.24-.095-.804.027-1.29.469-.34.315-.504.747-.597 1.161-.098.418-.21 1.18.488 1.452.222.095.422.123.624.113.702-.038 1.236-1.098 1.633-1.516.305-.333.358.124.405-.079m-1.074-.05c-.027.112-.13.549-.268.732-.092.13-.311.211-.437.211-.036 0-.257 0-.264-.325a2.225 2.225 0 0 1 .073-.512c.12-.515.258-.95.616-.95.28 0 .4.316.28.843M20.746 14.319a12.427 12.427 0 0 0-1.134.02l-.02-.04c.046-.2.097-.398.144-.6l.061-.275c.099-.425.194-.92.203-1.072.01-.09.042-.317-.216-.317-.113 0-.225.053-.341.107-.062.226-.192.863-.255 1.153-.126.61-.136.681-.193.983l-.04.041a12.904 12.904 0 0 0-1.156.02l-.024-.046c.088-.362.177-.728.262-1.091.224-.986.276-1.362.339-1.863l.04-.03c.52-.073.648-.088 1.212-.202l.043.053-.08.313a4.81 4.81 0 0 1 .281-.163c.264-.13.562-.17.724-.17.244 0 .516.069.632.355.105.254.033.567-.108 1.184l-.07.316c-.15.686-.17.812-.25 1.283l-.054.041zM25.133 10.61c-.079.359-.312.66-.61.806-.247.124-.549.134-.86.134h-.201l.015-.08.37-1.608.011-.082.005-.063.149.015.782.067c.302.117.426.418.34.81m-.487-1.68l-.375.003c-.974.012-1.364.008-1.524-.011l-.04.197-.348 1.618-.874 3.597c.85-.01 1.199-.01 1.345.006.034-.161.23-1.121.232-1.121 0 0 .168-.704.178-.73 0 0 .053-.073.106-.102h.078c.732 0 1.56 0 2.209-.477.441-.328.743-.81.877-1.398.035-.144.061-.315.061-.487 0-.225-.045-.447-.176-.62-.33-.464-.99-.472-1.75-.476M33.124 11.185l-.043-.05c-.556.113-.656.131-1.167.2l-.038.038-.005.024-.002-.009c-.38.877-.37.688-.679 1.378l-.003-.084-.077-1.497-.05-.05c-.581.113-.595.131-1.133.2l-.041.038c-.006.017-.006.037-.01.059l.004.007c.067.344.05.267.118.809.032.266.073.533.105.796.053.44.083.656.147 1.327-.363.6-.449.826-.798 1.352l.022.049c.524-.02.646-.02 1.035-.02l.084-.096c.294-.633 2.531-4.47 2.531-4.47M14.12 11.556c.298-.207.335-.493.085-.641-.254-.15-.7-.102-1 .105-.3.203-.333.49-.08.642.25.146.697.103.994-.106" fill="#FEFEFE"/><path d="M30.554 15.709l-.437.75c-.139.256-.395.447-.803.448l-.696-.012.203-.674h.137c.07 0 .121-.003.16-.023.036-.012.062-.04.09-.08l.258-.409h1.088z" fill="#FEFEFE"/></g></svg>PK
!<ñö '""+chrome/content/third-party/cc-logo-visa.svg<svg width="44" height="30" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path d="M22.8 9.786c-.025-1.96 1.765-3.053 3.113-3.703 1.385-.667 1.85-1.095 1.845-1.691-.01-.913-1.105-1.316-2.13-1.332-1.787-.027-2.826.478-3.652.86L21.332.938c.83-.378 2.364-.708 3.956-.722 3.735 0 6.18 1.824 6.193 4.653.014 3.59-5.02 3.79-4.985 5.395.012.486.481 1.005 1.51 1.138.508.066 1.914.117 3.506-.609l.626 2.884a9.623 9.623 0 0 1-3.329.605c-3.516 0-5.99-1.85-6.01-4.497m15.347 4.248a1.621 1.621 0 0 1-1.514-.998L31.296.428h3.733l.743 2.032h4.561l.431-2.032h3.29l-2.87 13.606h-3.038m.522-3.675l1.077-5.11h-2.95l1.873 5.11m-20.394 3.675L15.33.428h3.557l2.942 13.606h-3.556m-8.965-9.26L7.81 12.648c-.176.879-.87 1.386-1.64 1.386H.116l-.084-.395c1.242-.267 2.654-.697 3.51-1.157.523-.282.672-.527.844-1.196L7.224.428h3.76l5.763 13.606H13.01L9.31 4.774z" id="a"/><linearGradient x1="16.148%" y1="34.401%" x2="85.832%" y2="66.349%" id="b"><stop stop-color="#222357" offset="0%"/><stop stop-color="#254AA5" offset="100%"/></linearGradient></defs><g transform="matrix(1 0 0 -1 0 22.674)" fill="none" fill-rule="evenodd"><mask id="c" fill="#fff"><use xlink:href="#a"/></mask><path fill="url(#b)" fill-rule="nonzero" mask="url(#c)" d="M-4.669 12.849l44.237 16.12L49.63 1.929 5.395-14.19"/></g></svg>PK
!<Å±±chrome/res/FormAutofill.jsm/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

var EXPORTED_SYMBOLS = ["FormAutofill"];

ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.import("resource://gre/modules/Services.jsm");

const ADDRESSES_FIRST_TIME_USE_PREF = "extensions.formautofill.firstTimeUse";
const AUTOFILL_CREDITCARDS_AVAILABLE_PREF = "extensions.formautofill.creditCards.available";
const CREDITCARDS_USED_STATUS_PREF = "extensions.formautofill.creditCards.used";
const DEFAULT_REGION_PREF = "browser.search.region";
const ENABLED_AUTOFILL_ADDRESSES_PREF = "extensions.formautofill.addresses.enabled";
const ENABLED_AUTOFILL_CREDITCARDS_PREF = "extensions.formautofill.creditCards.enabled";
const SUPPORTED_COUNTRIES_PREF = "extensions.formautofill.supportedCountries";

XPCOMUtils.defineLazyPreferenceGetter(this, "logLevel", "extensions.formautofill.loglevel",
                                      "Warn");

// A logging helper for debug logging to avoid creating Console objects
// or triggering expensive JS -> C++ calls when debug logging is not
// enabled.
//
// Console objects, even natively-implemented ones, can consume a lot of
// memory, and since this code may run in every content process, that
// memory can add up quickly. And, even when debug-level messages are
// being ignored, console.debug() calls can be expensive.
//
// This helper avoids both of those problems by never touching the
// console object unless debug logging is enabled.
function debug() {
  if (logLevel.toLowerCase() == "debug") {
    this.log.debug(...arguments);
  }
}

var FormAutofill = {
  ENABLED_AUTOFILL_ADDRESSES_PREF,
  ENABLED_AUTOFILL_CREDITCARDS_PREF,
  ADDRESSES_FIRST_TIME_USE_PREF,
  CREDITCARDS_USED_STATUS_PREF,

  get isAutofillEnabled() { return FormAutofill.isAutofillAddressesEnabled || this.isAutofillCreditCardsEnabled; },
  get isAutofillCreditCardsEnabled() { return FormAutofill.isAutofillCreditCardsAvailable && FormAutofill._isAutofillCreditCardsEnabled; },

  defineLazyLogGetter(scope, logPrefix) {
    scope.debug = debug;

    XPCOMUtils.defineLazyGetter(scope, "log", () => {
      let ConsoleAPI = ChromeUtils.import("resource://gre/modules/Console.jsm", {}).ConsoleAPI;
      return new ConsoleAPI({
        maxLogLevelPref: "extensions.formautofill.loglevel",
        prefix: logPrefix,
      });
    });
  },
};

XPCOMUtils.defineLazyPreferenceGetter(FormAutofill,
                                      "DEFAULT_REGION", DEFAULT_REGION_PREF, "US");
XPCOMUtils.defineLazyPreferenceGetter(FormAutofill,
                                      "isAutofillAddressesEnabled", ENABLED_AUTOFILL_ADDRESSES_PREF);
XPCOMUtils.defineLazyPreferenceGetter(FormAutofill,
                                      "isAutofillCreditCardsAvailable", AUTOFILL_CREDITCARDS_AVAILABLE_PREF);
XPCOMUtils.defineLazyPreferenceGetter(FormAutofill,
                                      "_isAutofillCreditCardsEnabled", ENABLED_AUTOFILL_CREDITCARDS_PREF);
XPCOMUtils.defineLazyPreferenceGetter(FormAutofill,
                                      "isAutofillAddressesFirstTimeUse", ADDRESSES_FIRST_TIME_USE_PREF);
XPCOMUtils.defineLazyPreferenceGetter(FormAutofill,
                                      "AutofillCreditCardsUsedStatus", CREDITCARDS_USED_STATUS_PREF);
XPCOMUtils.defineLazyPreferenceGetter(FormAutofill,
                                      "supportedCountries", SUPPORTED_COUNTRIES_PREF, null, null,
                                      val => val.split(","));

XPCOMUtils.defineLazyGetter(FormAutofill, "countries", () => {
  return Services.intl.getRegions(undefined);
});
PK
!<í	þcÇZÇZ"chrome/res/FormAutofillContent.jsm/* 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/. */

/*
 * Form Autofill content process module.
 */

/* eslint-disable no-use-before-define */

"use strict";

var EXPORTED_SYMBOLS = ["FormAutofillContent"];

const Cm = Components.manager;

ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");

ChromeUtils.defineModuleGetter(this, "AddressResult",
                               "resource://formautofill/ProfileAutoCompleteResult.jsm");
ChromeUtils.defineModuleGetter(this, "CreditCardResult",
                               "resource://formautofill/ProfileAutoCompleteResult.jsm");
ChromeUtils.defineModuleGetter(this, "FormAutofill",
                               "resource://formautofill/FormAutofill.jsm");
ChromeUtils.defineModuleGetter(this, "FormAutofillHandler",
                               "resource://formautofill/FormAutofillHandler.jsm");
ChromeUtils.defineModuleGetter(this, "FormAutofillUtils",
                               "resource://formautofill/FormAutofillUtils.jsm");
ChromeUtils.defineModuleGetter(this, "FormLikeFactory",
                               "resource://gre/modules/FormLikeFactory.jsm");
ChromeUtils.defineModuleGetter(this, "InsecurePasswordUtils",
                               "resource://gre/modules/InsecurePasswordUtils.jsm");
ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils",
                               "resource://gre/modules/PrivateBrowsingUtils.jsm");

const formFillController = Cc["@mozilla.org/satchel/form-fill-controller;1"]
                             .getService(Ci.nsIFormFillController);
const autocompleteController = Cc["@mozilla.org/autocomplete/controller;1"]
                             .getService(Ci.nsIAutoCompleteController);

XPCOMUtils.defineLazyGetter(this, "ADDRESSES_COLLECTION_NAME",
                            () => FormAutofillUtils.ADDRESSES_COLLECTION_NAME);
XPCOMUtils.defineLazyGetter(this, "CREDITCARDS_COLLECTION_NAME",
                            () => FormAutofillUtils.CREDITCARDS_COLLECTION_NAME);
XPCOMUtils.defineLazyGetter(this, "FIELD_STATES",
                            () => FormAutofillUtils.FIELD_STATES);

// Register/unregister a constructor as a factory.
function AutocompleteFactory() {}
AutocompleteFactory.prototype = {
  register(targetConstructor) {
    let proto = targetConstructor.prototype;
    this._classID = proto.classID;

    let factory = XPCOMUtils._getFactory(targetConstructor);
    this._factory = factory;

    let registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
    registrar.registerFactory(proto.classID, proto.classDescription,
                              proto.contractID, factory);

    if (proto.classID2) {
      this._classID2 = proto.classID2;
      registrar.registerFactory(proto.classID2, proto.classDescription,
                                proto.contractID2, factory);
    }
  },

  unregister() {
    let registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
    registrar.unregisterFactory(this._classID, this._factory);
    if (this._classID2) {
      registrar.unregisterFactory(this._classID2, this._factory);
    }
    this._factory = null;
  },
};


/**
 * @constructor
 *
 * @implements {nsIAutoCompleteSearch}
 */
function AutofillProfileAutoCompleteSearch() {
  FormAutofill.defineLazyLogGetter(this, "AutofillProfileAutoCompleteSearch");
}
AutofillProfileAutoCompleteSearch.prototype = {
  classID: Components.ID("4f9f1e4c-7f2c-439e-9c9e-566b68bc187d"),
  contractID: "@mozilla.org/autocomplete/search;1?name=autofill-profiles",
  classDescription: "AutofillProfileAutoCompleteSearch",
  QueryInterface: ChromeUtils.generateQI([Ci.nsIAutoCompleteSearch]),

  // Begin nsIAutoCompleteSearch implementation

  /**
   * Searches for a given string and notifies a listener (either synchronously
   * or asynchronously) of the result
   *
   * @param {string} searchString the string to search for
   * @param {string} searchParam
   * @param {Object} previousResult a previous result to use for faster searchinig
   * @param {Object} listener the listener to notify when the search is complete
   */
  startSearch(searchString, searchParam, previousResult, listener) {
    let {activeInput, activeSection, activeFieldDetail, savedFieldNames} = FormAutofillContent;
    this.forceStop = false;

    this.debug("startSearch: for", searchString, "with input", activeInput);

    let isAddressField = FormAutofillUtils.isAddressField(activeFieldDetail.fieldName);
    let isInputAutofilled = activeFieldDetail.state == FIELD_STATES.AUTO_FILLED;
    let allFieldNames = activeSection.allFieldNames;
    let filledRecordGUID = activeSection.filledRecordGUID;
    let searchPermitted = isAddressField ?
                          FormAutofill.isAutofillAddressesEnabled :
                          FormAutofill.isAutofillCreditCardsEnabled;
    let AutocompleteResult = isAddressField ? AddressResult : CreditCardResult;
    let pendingSearchResult = null;

    ProfileAutocomplete.lastProfileAutoCompleteFocusedInput = activeInput;
    // Fallback to form-history if ...
    //   - specified autofill feature is pref off.
    //   - no profile can fill the currently-focused input.
    //   - the current form has already been populated.
    //   - (address only) less than 3 inputs are covered by all saved fields in the storage.
    if (!searchPermitted || !savedFieldNames.has(activeFieldDetail.fieldName) ||
        (!isInputAutofilled && filledRecordGUID) || (isAddressField &&
        allFieldNames.filter(field => savedFieldNames.has(field)).length < FormAutofillUtils.AUTOFILL_FIELDS_THRESHOLD)) {
      if (activeInput.autocomplete == "off") {
        // Create a dummy result as an empty search result.
        pendingSearchResult = new AutocompleteResult("", "", [], [], {});
      } else {
        pendingSearchResult = new Promise(resolve => {
          let formHistory = Cc["@mozilla.org/autocomplete/search;1?name=form-history"]
                            .createInstance(Ci.nsIAutoCompleteSearch);
          formHistory.startSearch(searchString, searchParam, previousResult, {
            onSearchResult: (_, result) => resolve(result),
          });
        });
      }
    } else if (isInputAutofilled) {
      pendingSearchResult = new AutocompleteResult(searchString, "", [], [], {isInputAutofilled});
    } else {
      let infoWithoutElement = {...activeFieldDetail};
      delete infoWithoutElement.elementWeakRef;

      let data = {
        collectionName: isAddressField ? ADDRESSES_COLLECTION_NAME : CREDITCARDS_COLLECTION_NAME,
        info: infoWithoutElement,
        searchString,
      };

      pendingSearchResult = this._getRecords(data).then((records) => {
        if (this.forceStop) {
          return null;
        }
        // Sort addresses by timeLastUsed for showing the lastest used address at top.
        records.sort((a, b) => b.timeLastUsed - a.timeLastUsed);

        let adaptedRecords = activeSection.getAdaptedProfiles(records);
        let handler = FormAutofillContent.activeHandler;
        let isSecure = InsecurePasswordUtils.isFormSecure(handler.form);

        return new AutocompleteResult(searchString,
                                      activeFieldDetail.fieldName,
                                      allFieldNames,
                                      adaptedRecords,
                                      {isSecure, isInputAutofilled});
      });
    }

    Promise.resolve(pendingSearchResult).then((result) => {
      listener.onSearchResult(this, result);
      ProfileAutocomplete.lastProfileAutoCompleteResult = result;
      // Reset AutoCompleteController's state at the end of startSearch to ensure that
      // none of form autofill result will be cached in other places and make the
      // result out of sync.
      autocompleteController.resetInternalState();
    });
  },

  /**
   * Stops an asynchronous search that is in progress
   */
  stopSearch() {
    ProfileAutocomplete.lastProfileAutoCompleteResult = null;
    this.forceStop = true;
  },

  /**
   * Get the records from parent process for AutoComplete result.
   *
   * @private
   * @param  {Object} data
   *         Parameters for querying the corresponding result.
   * @param  {string} data.collectionName
   *         The name used to specify which collection to retrieve records.
   * @param  {string} data.searchString
   *         The typed string for filtering out the matched records.
   * @param  {string} data.info
   *         The input autocomplete property's information.
   * @returns {Promise}
   *          Promise that resolves when addresses returned from parent process.
   */
  _getRecords(data) {
    this.debug("_getRecords with data:", data);
    return new Promise((resolve) => {
      Services.cpmm.addMessageListener("FormAutofill:Records", function getResult(result) {
        Services.cpmm.removeMessageListener("FormAutofill:Records", getResult);
        resolve(result.data);
      });

      Services.cpmm.sendAsyncMessage("FormAutofill:GetRecords", data);
    });
  },
};

let ProfileAutocomplete = {
  QueryInterface: ChromeUtils.generateQI([Ci.nsIObserver]),

  lastProfileAutoCompleteResult: null,
  lastProfileAutoCompleteFocusedInput: null,
  _registered: false,
  _factory: null,

  ensureRegistered() {
    if (this._registered) {
      return;
    }

    FormAutofill.defineLazyLogGetter(this, "ProfileAutocomplete");
    this.debug("ensureRegistered");
    this._factory = new AutocompleteFactory();
    this._factory.register(AutofillProfileAutoCompleteSearch);
    this._registered = true;

    Services.obs.addObserver(this, "autocomplete-will-enter-text");
  },

  ensureUnregistered() {
    if (!this._registered) {
      return;
    }

    this.debug("ensureUnregistered");
    this._factory.unregister();
    this._factory = null;
    this._registered = false;
    this._lastAutoCompleteResult = null;

    Services.obs.removeObserver(this, "autocomplete-will-enter-text");
  },

  observe(subject, topic, data) {
    switch (topic) {
      case "autocomplete-will-enter-text": {
        if (!FormAutofillContent.activeInput) {
          // The observer notification is for autocomplete in a different process.
          break;
        }
        this._fillFromAutocompleteRow(FormAutofillContent.activeInput);
        break;
      }
    }
  },

  _frameMMFromWindow(contentWindow) {
    return contentWindow.docShell.messageManager;
  },

  _getSelectedIndex(contentWindow) {
    let mm = this._frameMMFromWindow(contentWindow);
    let selectedIndexResult = mm.sendSyncMessage("FormAutoComplete:GetSelectedIndex", {});
    if (selectedIndexResult.length != 1 || !Number.isInteger(selectedIndexResult[0])) {
      throw new Error("Invalid autocomplete selectedIndex");
    }

    return selectedIndexResult[0];
  },

  _fillFromAutocompleteRow(focusedInput) {
    this.debug("_fillFromAutocompleteRow:", focusedInput);
    let formDetails = FormAutofillContent.activeFormDetails;
    if (!formDetails) {
      // The observer notification is for a different frame.
      return;
    }

    let selectedIndex = this._getSelectedIndex(focusedInput.ownerGlobal);
    if (selectedIndex == -1 ||
        !this.lastProfileAutoCompleteResult ||
        this.lastProfileAutoCompleteResult.getStyleAt(selectedIndex) != "autofill-profile") {
      return;
    }

    let profile = JSON.parse(this.lastProfileAutoCompleteResult.getCommentAt(selectedIndex));

    FormAutofillContent.activeHandler.autofillFormFields(profile);
  },

  _clearProfilePreview() {
    if (!this.lastProfileAutoCompleteFocusedInput || !FormAutofillContent.activeSection) {
      return;
    }

    FormAutofillContent.activeSection.clearPreviewedFormFields();
  },

  _previewSelectedProfile(selectedIndex) {
    if (!FormAutofillContent.activeInput || !FormAutofillContent.activeFormDetails) {
      // The observer notification is for a different process/frame.
      return;
    }

    if (!this.lastProfileAutoCompleteResult ||
        this.lastProfileAutoCompleteResult.getStyleAt(selectedIndex) != "autofill-profile") {
      return;
    }

    let profile = JSON.parse(this.lastProfileAutoCompleteResult.getCommentAt(selectedIndex));
    FormAutofillContent.activeSection.previewFormFields(profile);
  },
};

/**
 * Handles content's interactions for the process.
 *
 * NOTE: Declares it by "var" to make it accessible in unit tests.
 */
var FormAutofillContent = {
  QueryInterface: ChromeUtils.generateQI([Ci.nsIFormSubmitObserver]),
  /**
   * @type {WeakMap} mapping FormLike root HTML elements to FormAutofillHandler objects.
   */
  _formsDetails: new WeakMap(),

  /**
   * @type {Set} Set of the fields with usable values in any saved profile.
   */
  savedFieldNames: null,

  /**
   * @type {Object} The object where to store the active items, e.g. element,
   * handler, section, and field detail.
   */
  _activeItems: {},

  init() {
    FormAutofill.defineLazyLogGetter(this, "FormAutofillContent");

    Services.cpmm.addMessageListener("FormAutofill:enabledStatus", this);
    Services.cpmm.addMessageListener("FormAutofill:savedFieldNames", this);
    Services.obs.addObserver(this, "earlyformsubmit");

    let autofillEnabled = Services.cpmm.initialProcessData.autofillEnabled;
    // If storage hasn't be initialized yet autofillEnabled is undefined but we need to ensure
    // autocomplete is registered before the focusin so register it in this case as long as the
    // pref is true.
    let shouldEnableAutofill = autofillEnabled === undefined &&
                               (FormAutofill.isAutofillAddressesEnabled ||
                               FormAutofill.isAutofillCreditCardsEnabled);
    if (autofillEnabled || shouldEnableAutofill) {
      ProfileAutocomplete.ensureRegistered();
    }

    this.savedFieldNames =
      Services.cpmm.initialProcessData.autofillSavedFieldNames;
  },

  /**
   * Send the profile to parent for doorhanger and storage saving/updating.
   *
   * @param {Object} profile Submitted form's address/creditcard guid and record.
   * @param {Object} domWin Current content window.
   * @param {int} timeStartedFillingMS Time of form filling started.
   */
  _onFormSubmit(profile, domWin, timeStartedFillingMS) {
    let mm = this._messageManagerFromWindow(domWin);
    mm.sendAsyncMessage("FormAutofill:OnFormSubmit",
                        {profile, timeStartedFillingMS});
  },

  /**
   * Handle earlyformsubmit event and early return when:
   * 1. In private browsing mode.
   * 2. Could not map any autofill handler by form element.
   * 3. Number of filled fields is less than autofill threshold
   *
   * @param {HTMLElement} formElement Root element which receives earlyformsubmit event.
   * @param {Object} domWin Content window
   * @returns {boolean} Should always return true so form submission isn't canceled.
   */
  notify(formElement, domWin) {
    try {
      this.debug("Notifying form early submission");

      if (!FormAutofill.isAutofillEnabled) {
        this.debug("Form Autofill is disabled");
        return true;
      }

      if (domWin && PrivateBrowsingUtils.isContentWindowPrivate(domWin)) {
        this.debug("Ignoring submission in a private window");
        return true;
      }

      let handler = this._formsDetails.get(formElement);
      if (!handler) {
        this.debug("Form element could not map to an existing handler");
        return true;
      }

      let records = handler.createRecords();
      if (!Object.values(records).some(typeRecords => typeRecords.length)) {
        return true;
      }

      this._onFormSubmit(records, domWin, handler.timeStartedFillingMS);
    } catch (ex) {
      Cu.reportError(ex);
    }
    return true;
  },

  receiveMessage({name, data}) {
    switch (name) {
      case "FormAutofill:enabledStatus": {
        if (data) {
          ProfileAutocomplete.ensureRegistered();
        } else {
          ProfileAutocomplete.ensureUnregistered();
        }
        break;
      }
      case "FormAutofill:savedFieldNames": {
        this.savedFieldNames = data;
      }
    }
  },

  /**
   * Get the form's handler from cache which is created after page identified.
   *
   * @param {HTMLInputElement} element Focused input which triggered profile searching
   * @returns {Array<Object>|null}
   *          Return target form's handler from content cache
   *          (or return null if the information is not found in the cache).
   *
   */
  _getFormHandler(element) {
    if (!element) {
      return null;
    }
    let rootElement = FormLikeFactory.findRootForField(element);
    return this._formsDetails.get(rootElement);
  },

  /**
   * Get the active form's information from cache which is created after page
   * identified.
   *
   * @returns {Array<Object>|null}
   *          Return target form's information from content cache
   *          (or return null if the information is not found in the cache).
   *
   */
  get activeFormDetails() {
    let formHandler = this.activeHandler;
    return formHandler ? formHandler.fieldDetails : null;
  },

  /**
   * All active items should be updated according the active element of
   * `formFillController.focusedInput`. All of them including element,
   * handler, section, and field detail, can be retrieved by their own getters.
   *
   * @param {HTMLElement|null} element The active item should be updated based
   * on this or `formFillController.focusedInput` will be taken.
   */
  updateActiveInput(element) {
    element = element || formFillController.focusedInput;
    if (!element) {
      this._activeItems = {};
      return;
    }
    let handler = this._getFormHandler(element);
    if (handler) {
      handler.focusedInput = element;
    }
    this._activeItems = {
      handler,
      elementWeakRef: Cu.getWeakReference(element),
      section: handler ? handler.activeSection : null,
      fieldDetail: null,
    };
  },

  get activeInput() {
    let elementWeakRef = this._activeItems.elementWeakRef;
    return elementWeakRef ? elementWeakRef.get() : null;
  },

  get activeHandler() {
    return this._activeItems.handler;
  },

  get activeSection() {
    return this._activeItems.section;
  },

  /**
   * Get the active input's information from cache which is created after page
   * identified.
   *
   * @returns {Object|null}
   *          Return the active input's information that cloned from content cache
   *          (or return null if the information is not found in the cache).
   */
  get activeFieldDetail() {
    if (!this._activeItems.fieldDetail) {
      let formDetails = this.activeFormDetails;
      if (!formDetails) {
        return null;
      }
      for (let detail of formDetails) {
        let detailElement = detail.elementWeakRef.get();
        if (detailElement && this.activeInput == detailElement) {
          this._activeItems.fieldDetail = detail;
          break;
        }
      }
    }
    return this._activeItems.fieldDetail;
  },

  identifyAutofillFields(element) {
    this.debug("identifyAutofillFields:", String(element.ownerDocument.location));

    if (!this.savedFieldNames) {
      this.debug("identifyAutofillFields: savedFieldNames are not known yet");
      Services.cpmm.sendAsyncMessage("FormAutofill:InitStorage");
    }

    let formHandler = this._getFormHandler(element);
    if (!formHandler) {
      let formLike = FormLikeFactory.createFromField(element);
      formHandler = new FormAutofillHandler(formLike);
    } else if (!formHandler.updateFormIfNeeded(element)) {
      this.debug("No control is removed or inserted since last collection.");
      return;
    }

    let validDetails = formHandler.collectFormFields();

    this._formsDetails.set(formHandler.form.rootElement, formHandler);
    this.debug("Adding form handler to _formsDetails:", formHandler);

    validDetails.forEach(detail =>
      this._markAsAutofillField(detail.elementWeakRef.get())
    );
  },

  clearForm() {
    let focusedInput = this.activeInput || ProfileAutocomplete._lastAutoCompleteFocusedInput;
    if (!focusedInput) {
      return;
    }

    this.activeSection.clearPopulatedForm();
  },

  previewProfile(doc) {
    let docWin = doc.ownerGlobal;
    let selectedIndex = ProfileAutocomplete._getSelectedIndex(docWin);
    let lastAutoCompleteResult = ProfileAutocomplete.lastProfileAutoCompleteResult;
    let focusedInput = this.activeInput;
    let mm = this._messageManagerFromWindow(docWin);

    if (selectedIndex === -1 ||
        !focusedInput ||
        !lastAutoCompleteResult ||
        lastAutoCompleteResult.getStyleAt(selectedIndex) != "autofill-profile") {
      mm.sendAsyncMessage("FormAutofill:UpdateWarningMessage", {});

      ProfileAutocomplete._clearProfilePreview();
    } else {
      let focusedInputDetails = this.activeFieldDetail;
      let profile = JSON.parse(lastAutoCompleteResult.getCommentAt(selectedIndex));
      let allFieldNames = FormAutofillContent.activeSection.allFieldNames;
      let profileFields = allFieldNames.filter(fieldName => !!profile[fieldName]);

      let focusedCategory = FormAutofillUtils.getCategoryFromFieldName(focusedInputDetails.fieldName);
      let categories = FormAutofillUtils.getCategoriesFromFieldNames(profileFields);
      mm.sendAsyncMessage("FormAutofill:UpdateWarningMessage", {
        focusedCategory,
        categories,
      });

      ProfileAutocomplete._previewSelectedProfile(selectedIndex);
    }
  },

  onPopupClosed(selectedRowStyle) {
    ProfileAutocomplete._clearProfilePreview();

    let lastAutoCompleteResult = ProfileAutocomplete.lastProfileAutoCompleteResult;
    let focusedInput = FormAutofillContent.activeInput;
    if (lastAutoCompleteResult && FormAutofillContent._keyDownEnterForInput &&
        focusedInput === FormAutofillContent._keyDownEnterForInput &&
        focusedInput === ProfileAutocomplete.lastProfileAutoCompleteFocusedInput) {
      if (selectedRowStyle == "autofill-footer") {
        Services.cpmm.sendAsyncMessage("FormAutofill:OpenPreferences");
      } else if (selectedRowStyle == "autofill-clear-button") {
        FormAutofillContent.clearForm();
      }
    }
  },

  _markAsAutofillField(field) {
    // Since Form Autofill popup is only for input element, any non-Input
    // element should be excluded here.
    if (!field || ChromeUtils.getClassName(field) !== "HTMLInputElement") {
      return;
    }

    formFillController.markAsAutofillField(field);
  },

  _messageManagerFromWindow(win) {
    return win.docShell.messageManager;
  },

  _onKeyDown(e) {
    delete FormAutofillContent._keyDownEnterForInput;
    let lastAutoCompleteResult = ProfileAutocomplete.lastProfileAutoCompleteResult;
    let focusedInput = FormAutofillContent.activeInput;
    if (e.keyCode != e.DOM_VK_RETURN || !lastAutoCompleteResult ||
        !focusedInput || focusedInput != ProfileAutocomplete.lastProfileAutoCompleteFocusedInput) {
      return;
    }
    FormAutofillContent._keyDownEnterForInput = focusedInput;
  },
};


FormAutofillContent.init();
PK
!<lž~;~;%chrome/res/FormAutofillDoorhanger.jsm/* 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/. */

/*
 * Implements doorhanger singleton that wraps up the PopupNotifications and handles
 * the doorhager UI for formautofill related features.
 */

"use strict";

var EXPORTED_SYMBOLS = ["FormAutofillDoorhanger"];

ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://formautofill/FormAutofill.jsm");
ChromeUtils.import("resource://formautofill/FormAutofillUtils.jsm");

this.log = null;
FormAutofill.defineLazyLogGetter(this, EXPORTED_SYMBOLS[0]);

const GetStringFromName = FormAutofillUtils.stringBundle.GetStringFromName;
const formatStringFromName = FormAutofillUtils.stringBundle.formatStringFromName;
const brandShortName = FormAutofillUtils.brandBundle.GetStringFromName("brandShortName");
let changeAutofillOptsKey = "changeAutofillOptions";
let autofillOptsKey = "autofillOptionsLink";
let autofillSecurityOptionsKey = "autofillSecurityOptionsLink";
if (AppConstants.platform == "macosx") {
  changeAutofillOptsKey += "OSX";
  autofillOptsKey += "OSX";
  autofillSecurityOptionsKey += "OSX";
}

const CONTENT = {
  firstTimeUse: {
    notificationId: "autofill-address",
    message: formatStringFromName("saveAddressesMessage", [brandShortName], 1),
    anchor: {
      id: "autofill-address-notification-icon",
      URL: "chrome://formautofill/content/formfill-anchor.svg",
      tooltiptext: GetStringFromName("openAutofillMessagePanel"),
    },
    mainAction: {
      label: GetStringFromName(changeAutofillOptsKey),
      accessKey: GetStringFromName("changeAutofillOptionsAccessKey"),
      callbackState: "open-pref",
      disableHighlight: true,
    },
    options: {
      persistWhileVisible: true,
      popupIconURL: "chrome://formautofill/content/icon-address-save.svg",
      checkbox: {
        get checked() {
          return Services.prefs.getBoolPref("services.sync.engine.addresses");
        },
        get label() {
          // If sync account is not set, return null label to hide checkbox
          return Services.prefs.prefHasUserValue("services.sync.username") ?
            GetStringFromName("addressesSyncCheckbox") : null;
        },
        callback(event) {
          let checked = event.target.checked;
          Services.prefs.setBoolPref("services.sync.engine.addresses", checked);
          log.debug("Set addresses sync to", checked);
        },
      },
      hideClose: true,
    },
  },
  updateAddress: {
    notificationId: "autofill-address",
    message: GetStringFromName("updateAddressMessage"),
    descriptionLabel: GetStringFromName("updateAddressDescriptionLabel"),
    descriptionIcon: false,
    linkMessage: GetStringFromName(autofillOptsKey),
    spotlightURL: "about:preferences#privacy-address-autofill",
    anchor: {
      id: "autofill-address-notification-icon",
      URL: "chrome://formautofill/content/formfill-anchor.svg",
      tooltiptext: GetStringFromName("openAutofillMessagePanel"),
    },
    mainAction: {
      label: GetStringFromName("updateAddressLabel"),
      accessKey: GetStringFromName("updateAddressAccessKey"),
      callbackState: "update",
    },
    secondaryActions: [{
      label: GetStringFromName("createAddressLabel"),
      accessKey: GetStringFromName("createAddressAccessKey"),
      callbackState: "create",
    }],
    options: {
      persistWhileVisible: true,
      popupIconURL: "chrome://formautofill/content/icon-address-update.svg",
      hideClose: true,
    },
  },
  addCreditCard: {
    notificationId: "autofill-credit-card",
    message: formatStringFromName("saveCreditCardMessage", [brandShortName], 1),
    descriptionLabel: GetStringFromName("saveCreditCardDescriptionLabel"),
    descriptionIcon: true,
    linkMessage: GetStringFromName(autofillSecurityOptionsKey),
    spotlightURL: "about:preferences#privacy-credit-card-autofill",
    anchor: {
      id: "autofill-credit-card-notification-icon",
      URL: "chrome://formautofill/content/formfill-anchor.svg",
      tooltiptext: GetStringFromName("openAutofillMessagePanel"),
    },
    mainAction: {
      label: GetStringFromName("saveCreditCardLabel"),
      accessKey: GetStringFromName("saveCreditCardAccessKey"),
      callbackState: "save",
    },
    secondaryActions: [{
      label: GetStringFromName("cancelCreditCardLabel"),
      accessKey: GetStringFromName("cancelCreditCardAccessKey"),
      callbackState: "cancel",
    }, {
      label: GetStringFromName("neverSaveCreditCardLabel"),
      accessKey: GetStringFromName("neverSaveCreditCardAccessKey"),
      callbackState: "disable",
    }],
    options: {
      persistWhileVisible: true,
      popupIconURL: "chrome://formautofill/content/icon-credit-card.svg",
      hideClose: true,
      checkbox: {
        get checked() {
          return Services.prefs.getBoolPref("services.sync.engine.creditcards");
        },
        get label() {
          // Only set the label when the fallowing conditions existed:
          // - sync account is set
          // - credit card sync is disabled
          // - credit card sync is available
          // otherwise return null label to hide checkbox.
          return Services.prefs.prefHasUserValue("services.sync.username") &&
            !Services.prefs.getBoolPref("services.sync.engine.creditcards") &&
            Services.prefs.getBoolPref("services.sync.engine.creditcards.available") ?
            GetStringFromName("creditCardsSyncCheckbox") : null;
        },
        callback(event) {
          let {secondaryButton, menubutton} = event.target.parentNode.parentNode.parentNode;
          let checked = event.target.checked;
          Services.prefs.setBoolPref("services.sync.engine.creditcards", checked);
          secondaryButton.disabled = checked;
          menubutton.disabled = checked;
          log.debug("Set creditCard sync to", checked);
        },
      },
    },
  },
  updateCreditCard: {
    notificationId: "autofill-credit-card",
    message: GetStringFromName("updateCreditCardMessage"),
    descriptionLabel: GetStringFromName("updateCreditCardDescriptionLabel"),
    descriptionIcon: true,
    linkMessage: GetStringFromName(autofillOptsKey),
    spotlightURL: "about:preferences#privacy-credit-card-autofill",
    anchor: {
      id: "autofill-credit-card-notification-icon",
      URL: "chrome://formautofill/content/formfill-anchor.svg",
      tooltiptext: GetStringFromName("openAutofillMessagePanel"),
    },
    mainAction: {
      label: GetStringFromName("updateCreditCardLabel"),
      accessKey: GetStringFromName("updateCreditCardAccessKey"),
      callbackState: "update",
    },
    secondaryActions: [{
      label: GetStringFromName("createCreditCardLabel"),
      accessKey: GetStringFromName("createCreditCardAccessKey"),
      callbackState: "create",
    }],
    options: {
      persistWhileVisible: true,
      popupIconURL: "chrome://formautofill/content/icon-credit-card.svg",
      hideClose: true,
    },
  },
};

let FormAutofillDoorhanger = {
  /**
   * Generate the main action and secondary actions from content parameters and
   * promise resolve.
   *
   * @private
   * @param  {Object} mainActionParams
   *         Parameters for main action.
   * @param  {Array<Object>} secondaryActionParams
   *         Array of the parameters for secondary actions.
   * @param  {Function} resolve Should be called in action callback.
   * @returns {Array<Object>}
              Return the mainAction and secondary actions in an array for showing doorhanger
   */
  _createActions(mainActionParams, secondaryActionParams, resolve) {
    if (!mainActionParams) {
      return [null, null];
    }

    let {label, accessKey, disableHighlight, callbackState} = mainActionParams;
    let callback = resolve.bind(null, callbackState);
    let mainAction = {label, accessKey, callback, disableHighlight};

    if (!secondaryActionParams) {
      return [mainAction, null];
    }

    let secondaryActions = [];
    for (let params of secondaryActionParams) {
      let cb = resolve.bind(null, params.callbackState);
      secondaryActions.push({
        label: params.label,
        accessKey: params.accessKey,
        callback: cb,
      });
    }

    return [mainAction, secondaryActions];
  },
  _getNotificationElm(browser, id) {
    let notificationId = id + "-notification";
    let chromeDoc = browser.ownerDocument;
    return chromeDoc.getElementById(notificationId);
  },
  /**
   * Append the link label element to the popupnotificationcontent.
   * @param  {XULElement} content
   *         popupnotificationcontent
   * @param  {string} message
   *         The localized string for link title.
   * @param  {string} link
   *         Makes it possible to open and highlight a section in preferences
   */
  _appendPrivacyPanelLink(content, message, link) {
    let chromeDoc = content.ownerDocument;
    let privacyLinkElement = chromeDoc.createXULElement("label");
    privacyLinkElement.className = "text-link";
    privacyLinkElement.setAttribute("useoriginprincipal", true);
    privacyLinkElement.setAttribute("href", link || "about:preferences#privacy-form-autofill");
    privacyLinkElement.setAttribute("value", message);
    content.appendChild(privacyLinkElement);
  },

  /**
   * Append the description section to the popupnotificationcontent.
   * @param  {XULElement} content
   *         popupnotificationcontent
   * @param  {string} descriptionLabel
   *         The label showing above description.
   * @param  {string} descriptionIcon
   *         The src of description icon.
   */
  _appendDescription(content, descriptionLabel, descriptionIcon) {
    let chromeDoc = content.ownerDocument;
    let docFragment = chromeDoc.createDocumentFragment();

    let descriptionLabelElement = chromeDoc.createXULElement("label");
    descriptionLabelElement.setAttribute("value", descriptionLabel);
    docFragment.appendChild(descriptionLabelElement);

    let descriptionWrapper = chromeDoc.createXULElement("hbox");
    descriptionWrapper.className = "desc-message-box";

    if (descriptionIcon) {
      let descriptionIconElement = chromeDoc.createXULElement("image");
      descriptionWrapper.appendChild(descriptionIconElement);
    }

    let descriptionElement = chromeDoc.createXULElement("description");
    descriptionWrapper.appendChild(descriptionElement);
    docFragment.appendChild(descriptionWrapper);

    content.appendChild(docFragment);
  },

  _updateDescription(content, description) {
    content.querySelector("description").textContent = description;
  },

  /**
   * Create an image element for notification anchor if it doesn't already exist.
   * @param  {XULElement} browser
   *         Target browser element for showing doorhanger.
   * @param  {Object} anchor
   *         Anchor options for setting the anchor element.
   * @param  {string} anchor.id
   *         ID of the anchor element.
   * @param  {string} anchor.URL
   *         Path of the icon asset.
   * @param  {string} anchor.tooltiptext
   *         Tooltip string for the anchor.
   */
  _setAnchor(browser, anchor) {
    let chromeDoc = browser.ownerDocument;
    let {id, URL, tooltiptext} = anchor;
    let anchorEt = chromeDoc.getElementById(id);
    if (!anchorEt) {
      let notificationPopupBox =
        chromeDoc.getElementById("notification-popup-box");
      // Icon shown on URL bar
      let anchorElement = chromeDoc.createXULElement("image");
      anchorElement.id = id;
      anchorElement.setAttribute("src", URL);
      anchorElement.classList.add("notification-anchor-icon");
      anchorElement.setAttribute("role", "button");
      anchorElement.setAttribute("tooltiptext", tooltiptext);
      notificationPopupBox.appendChild(anchorElement);
    }
  },
  _addCheckboxListener(browser, {notificationId, options}) {
    if (!options.checkbox) {
      return;
    }
    let {checkbox} = this._getNotificationElm(browser, notificationId);

    if (checkbox && !checkbox.hidden) {
      checkbox.addEventListener("command", options.checkbox.callback);
    }
  },
  _removeCheckboxListener(browser, {notificationId, options}) {
    if (!options.checkbox) {
      return;
    }
    let {checkbox} = this._getNotificationElm(browser, notificationId);

    if (checkbox && !checkbox.hidden) {
      checkbox.removeEventListener("command", options.checkbox.callback);
    }
  },
  /**
   * Show different types of doorhanger by leveraging PopupNotifications.
   * @param  {XULElement} browser
   *         Target browser element for showing doorhanger.
   * @param  {string} type
   *         The type of the doorhanger. There will have first time use/update/credit card.
   * @param  {string} description
   *         The message that provides more information on doorhanger.
   * @returns {Promise}
              Resolved with action type when action callback is triggered.
   */
  async show(browser, type, description) {
    log.debug("show doorhanger with type:", type);
    return new Promise((resolve) => {
      let {
        notificationId,
        message,
        descriptionLabel,
        descriptionIcon,
        linkMessage,
        spotlightURL,
        anchor,
        mainAction,
        secondaryActions,
        options,
      } = CONTENT[type];

      const {
        ownerGlobal: chromeWin,
        ownerDocument: chromeDoc,
      } = browser;
      options.eventCallback = (topic) => {
        log.debug("eventCallback:", topic);

        if (topic == "removed" || topic == "dismissed") {
          this._removeCheckboxListener(browser, {notificationId, options});
          return;
        }

        // The doorhanger is customizable only when notification box is shown
        if (topic != "shown") {
          return;
        }
        this._addCheckboxListener(browser, {notificationId, options});

        // There's no preferences link or other customization in first time use doorhanger.
        if (type == "firstTimeUse") {
          return;
        }

        const notificationElementId = notificationId + "-notification";
        const notification = chromeDoc.getElementById(notificationElementId);
        const notificationContent = notification.querySelector("popupnotificationcontent") ||
                                    chromeDoc.createXULElement("popupnotificationcontent");
        if (!notification.contains(notificationContent)) {
          notificationContent.setAttribute("orient", "vertical");
          this._appendDescription(notificationContent, descriptionLabel, descriptionIcon);
          this._appendPrivacyPanelLink(notificationContent, linkMessage, spotlightURL);
          notification.append(notificationContent);
        }
        this._updateDescription(notificationContent, description);
      };
      this._setAnchor(browser, anchor);
      chromeWin.PopupNotifications.show(
        browser,
        notificationId,
        message,
        anchor.id,
        ...this._createActions(mainAction, secondaryActions, resolve),
        options,
      );
    });
  },
};
PK
!<îÌh±l‰l‰"chrome/res/FormAutofillHandler.jsm/* 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/. */

/*
 * Defines a handler object to represent forms that autofill can handle.
 */

"use strict";

var EXPORTED_SYMBOLS = ["FormAutofillHandler"];

ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.import("resource://formautofill/FormAutofill.jsm");

ChromeUtils.defineModuleGetter(this, "FormAutofillUtils",
                               "resource://formautofill/FormAutofillUtils.jsm");
ChromeUtils.defineModuleGetter(this, "FormAutofillHeuristics",
                               "resource://formautofill/FormAutofillHeuristics.jsm");
ChromeUtils.defineModuleGetter(this, "FormLikeFactory",
                               "resource://gre/modules/FormLikeFactory.jsm");

XPCOMUtils.defineLazyGetter(this, "reauthPasswordPromptMessage", () => {
  const brandShortName = FormAutofillUtils.brandBundle.GetStringFromName("brandShortName");
  return FormAutofillUtils.stringBundle.formatStringFromName(
    `useCreditCardPasswordPrompt.${AppConstants.platform}`, [brandShortName], 1);
});

this.log = null;
FormAutofill.defineLazyLogGetter(this, EXPORTED_SYMBOLS[0]);

const {FIELD_STATES} = FormAutofillUtils;

class FormAutofillSection {
  constructor(fieldDetails, winUtils) {
    this.fieldDetails = fieldDetails;
    this.filledRecordGUID = null;
    this.winUtils = winUtils;

    /**
     * Enum for form autofill MANUALLY_MANAGED_STATES values
     */
    this._FIELD_STATE_ENUM = {
      // not themed
      [FIELD_STATES.NORMAL]: null,
      // highlighted
      [FIELD_STATES.AUTO_FILLED]: "-moz-autofill",
      // highlighted && grey color text
      [FIELD_STATES.PREVIEW]: "-moz-autofill-preview",
    };

    if (!this.isValidSection()) {
      this.fieldDetails = [];
      log.debug(`Ignoring ${this.constructor.name} related fields since it is an invalid section`);
    }

    this._cacheValue = {
      allFieldNames: null,
      matchingSelectOption: null,
    };
  }

  /*
   * Examine the section is a valid section or not based on its fieldDetails or
   * other information. This method must be overrided.
   *
   * @returns {boolean} True for a valid section, otherwise false
   *
   */
  isValidSection() {
    throw new TypeError("isValidSection method must be overrided");
  }

  /*
   * Examine the section is an enabled section type or not based on its
   * preferences. This method must be overrided.
   *
   * @returns {boolean} True for an enabled section type, otherwise false
   *
   */
  isEnabled() {
    throw new TypeError("isEnabled method must be overrided");
  }

  /*
   * Examine the section is createable for storing the profile. This method
   * must be overrided.
   *
   * @param {Object} record The record for examining createable
   * @returns {boolean} True for the record is createable, otherwise false
   *
   */
  isRecordCreatable(record) {
    throw new TypeError("isRecordCreatable method must be overrided");
  }

  /**
   * Override this method if the profile is needed to apply some transformers.
   *
   * @param {Object} profile
   *        A profile should be converted based on the specific requirement.
   */
  applyTransformers(profile) {}

  /**
   * Override this method if the profile is needed to be customized for
   * previewing values.
   *
   * @param {Object} profile
   *        A profile for pre-processing before previewing values.
   */
  preparePreviewProfile(profile) {}

  /**
   * Override this method if the profile is needed to be customized for filling
   * values.
   *
   * @param {Object} profile
   *        A profile for pre-processing before filling values.
   * @returns {boolean} Whether the profile should be filled.
   */
  async prepareFillingProfile(profile) {
    return true;
  }

  /*
   * Override this methid if any data for `createRecord` is needed to be
   * normailized before submitting the record.
   *
   * @param {Object} profile
   *        A record for normalization.
   */
  normalizeCreatingRecord(data) {}

  /*
   * Override this method if there is any field value needs to compute for a
   * specific case. Return the original value in the default case.
   * @param {String} value
   *        The original field value.
   * @param {Object} fieldDetail
   *        A fieldDetail of the related element.
   * @param {HTMLElement} element
   *        A element for checking converting value.
   *
   * @returns {String}
   *          A string of the converted value.
   */
  computeFillingValue(value, fieldName, element) {
    return value;
  }

  set focusedInput(element) {
    this._focusedDetail = this.getFieldDetailByElement(element);
  }

  getFieldDetailByElement(element) {
    return this.fieldDetails.find(
      detail => detail.elementWeakRef.get() == element
    );
  }

  get allFieldNames() {
    if (!this._cacheValue.allFieldNames) {
      this._cacheValue.allFieldNames = this.fieldDetails.map(record => record.fieldName);
    }
    return this._cacheValue.allFieldNames;
  }

  getFieldDetailByName(fieldName) {
    return this.fieldDetails.find(detail => detail.fieldName == fieldName);
  }

  matchSelectOptions(profile) {
    if (!this._cacheValue.matchingSelectOption) {
      this._cacheValue.matchingSelectOption = new WeakMap();
    }

    for (let fieldName in profile) {
      let fieldDetail = this.getFieldDetailByName(fieldName);
      if (!fieldDetail) {
        continue;
      }

      let element = fieldDetail.elementWeakRef.get();
      if (ChromeUtils.getClassName(element) !== "HTMLSelectElement") {
        continue;
      }

      let cache = this._cacheValue.matchingSelectOption.get(element) || {};
      let value = profile[fieldName];
      if (cache[value] && cache[value].get()) {
        continue;
      }

      let option = FormAutofillUtils.findSelectOption(element, profile, fieldName);
      if (option) {
        cache[value] = Cu.getWeakReference(option);
        this._cacheValue.matchingSelectOption.set(element, cache);
      } else {
        if (cache[value]) {
          delete cache[value];
          this._cacheValue.matchingSelectOption.set(element, cache);
        }
        // Delete the field so the phishing hint won't treat it as a "also fill"
        // field.
        delete profile[fieldName];
      }
    }
  }

  adaptFieldMaxLength(profile) {
    for (let key in profile) {
      let detail = this.getFieldDetailByName(key);
      if (!detail) {
        continue;
      }

      let element = detail.elementWeakRef.get();
      if (!element) {
        continue;
      }

      let maxLength = element.maxLength;
      if (maxLength === undefined || maxLength < 0 || profile[key].length <= maxLength) {
        continue;
      }

      if (maxLength) {
        profile[key] = profile[key].substr(0, maxLength);
      } else {
        delete profile[key];
      }
    }
  }

  getAdaptedProfiles(originalProfiles) {
    for (let profile of originalProfiles) {
      this.applyTransformers(profile);
    }
    return originalProfiles;
  }

  /**
   * Processes form fields that can be autofilled, and populates them with the
   * profile provided by backend.
   *
   * @param {Object} profile
   *        A profile to be filled in.
   */
  async autofillFields(profile) {
    let focusedDetail = this._focusedDetail;
    if (!focusedDetail) {
      throw new Error("No fieldDetail for the focused input.");
    }

    if (!await this.prepareFillingProfile(profile)) {
      log.debug("profile cannot be filled", profile);
      return;
    }
    log.debug("profile in autofillFields:", profile);

    this.filledRecordGUID = profile.guid;
    for (let fieldDetail of this.fieldDetails) {
      // Avoid filling field value in the following cases:
      // 1. a non-empty input field for an unfocused input
      // 2. the invalid value set
      // 3. value already chosen in select element

      let element = fieldDetail.elementWeakRef.get();
      if (!element) {
        continue;
      }

      element.previewValue = "";
      let value = profile[fieldDetail.fieldName];

      if (ChromeUtils.getClassName(element) === "HTMLInputElement" && value) {
        // For the focused input element, it will be filled with a valid value
        // anyway.
        // For the others, the fields should be only filled when their values
        // are empty.
        let focusedInput = focusedDetail.elementWeakRef.get();
        if (element == focusedInput ||
            (element != focusedInput && !element.value)) {
          element.setUserInput(value);
          this._changeFieldState(fieldDetail, FIELD_STATES.AUTO_FILLED);
        }
      } else if (ChromeUtils.getClassName(element) === "HTMLSelectElement") {
        let cache = this._cacheValue.matchingSelectOption.get(element) || {};
        let option = cache[value] && cache[value].get();
        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.Event("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, FIELD_STATES.AUTO_FILLED);
      }
    }
  }

  /**
   * Populates result to the preview layers with given profile.
   *
   * @param {Object} profile
   *        A profile to be previewed with
   */
  previewFormFields(profile) {
    log.debug("preview profile: ", profile);

    this.preparePreviewProfile(profile);

    for (let fieldDetail of this.fieldDetails) {
      let element = fieldDetail.elementWeakRef.get();
      let value = profile[fieldDetail.fieldName] || "";

      // Skip the field that is null
      if (!element) {
        continue;
      }

      if (ChromeUtils.getClassName(element) === "HTMLSelectElement") {
        // Unlike text input, select element is always previewed even if
        // the option is already selected.
        if (value) {
          let cache = this._cacheValue.matchingSelectOption.get(element) || {};
          let option = cache[value] && cache[value].get();
          if (option) {
            value = option.text || "";
          } else {
            value = "";
          }
        }
      } else if (element.value) {
        // Skip the field if it already has text entered.
        continue;
      }
      element.previewValue = value;
      this._changeFieldState(fieldDetail, value ? FIELD_STATES.PREVIEW : FIELD_STATES.NORMAL);
    }
  }

  /**
   * Clear preview text and background highlight of all fields.
   */
  clearPreviewedFormFields() {
    log.debug("clear previewed fields in:", this.form);

    for (let fieldDetail of this.fieldDetails) {
      let element = fieldDetail.elementWeakRef.get();
      if (!element) {
        log.warn(fieldDetail.fieldName, "is unreachable");
        continue;
      }

      element.previewValue = "";

      // We keep the state if this field has
      // already been auto-filled.
      if (fieldDetail.state == FIELD_STATES.AUTO_FILLED) {
        continue;
      }

      this._changeFieldState(fieldDetail, FIELD_STATES.NORMAL);
    }
  }

  /**
   * Clear value and highlight style of all filled fields.
   */
  clearPopulatedForm() {
    for (let fieldDetail of this.fieldDetails) {
      let element = fieldDetail.elementWeakRef.get();
      if (!element) {
        log.warn(fieldDetail.fieldName, "is unreachable");
        continue;
      }

      // Only reset value for input element.
      if (fieldDetail.state == FIELD_STATES.AUTO_FILLED &&
          ChromeUtils.getClassName(element) === "HTMLInputElement") {
        element.setUserInput("");
      }
    }
  }

  /**
   * Change the state of a field to correspond with different presentations.
   *
   * @param {Object} fieldDetail
   *        A fieldDetail of which its element is about to update the state.
   * @param {string} nextState
   *        Used to determine the next state
   */
  _changeFieldState(fieldDetail, nextState) {
    let element = fieldDetail.elementWeakRef.get();

    if (!element) {
      log.warn(fieldDetail.fieldName, "is unreachable while changing state");
      return;
    }
    if (!(nextState in this._FIELD_STATE_ENUM)) {
      log.warn(fieldDetail.fieldName, "is trying to change to an invalid state");
      return;
    }
    if (fieldDetail.state == nextState) {
      return;
    }

    for (let [state, mmStateValue] of Object.entries(this._FIELD_STATE_ENUM)) {
      // The NORMAL state is simply the absence of other manually
      // managed states so we never need to add or remove it.
      if (!mmStateValue) {
        continue;
      }

      if (state == nextState) {
        this.winUtils.addManuallyManagedState(element, mmStateValue);
      } else {
        this.winUtils.removeManuallyManagedState(element, mmStateValue);
      }
    }

    switch (nextState) {
      case FIELD_STATES.NORMAL: {
        if (fieldDetail.state == FIELD_STATES.AUTO_FILLED) {
          element.removeEventListener("input", this, {mozSystemGroup: true});
        }
        break;
      }
      case FIELD_STATES.AUTO_FILLED: {
        element.addEventListener("input", this, {mozSystemGroup: true});
        break;
      }
    }

    fieldDetail.state = nextState;
  }

  resetFieldStates() {
    for (let fieldDetail of this.fieldDetails) {
      const element = fieldDetail.elementWeakRef.get();
      element.removeEventListener("input", this, {mozSystemGroup: true});
      this._changeFieldState(fieldDetail, FIELD_STATES.NORMAL);
    }
    this.filledRecordGUID = null;
  }

  isFilled() {
    return !!this.filledRecordGUID;
  }

  /**
   * Return the record that is converted from `fieldDetails` and only valid
   * form record is included.
   *
   * @returns {Object|null}
   *          A record object consists of three properties:
   *            - guid: The id of the previously-filled profile or null if omitted.
   *            - record: A valid record converted from details with trimmed result.
   *            - untouchedFields: Fields that aren't touched after autofilling.
   *          Return `null` for any uncreatable or invalid record.
   */
  createRecord() {
    let details = this.fieldDetails;
    if (!this.isEnabled() || !details || details.length == 0) {
      return null;
    }

    let data = {
      guid: this.filledRecordGUID,
      record: {},
      untouchedFields: [],
    };

    details.forEach(detail => {
      let element = detail.elementWeakRef.get();
      // Remove the unnecessary spaces
      let value = element && element.value.trim();
      value = this.computeFillingValue(value, detail, element);

      if (!value || value.length > FormAutofillUtils.MAX_FIELD_VALUE_LENGTH) {
        // Keep the property and preserve more information for updating
        data.record[detail.fieldName] = "";
        return;
      }

      data.record[detail.fieldName] = value;

      if (detail.state == FIELD_STATES.AUTO_FILLED) {
        data.untouchedFields.push(detail.fieldName);
      }
    });

    this.normalizeCreatingRecord(data);

    if (!this.isRecordCreatable(data.record)) {
      return null;
    }

    return data;
  }

  handleEvent(event) {
    switch (event.type) {
      case "input": {
        if (!event.isTrusted) {
          return;
        }
        const target = event.target;
        const targetFieldDetail = this.getFieldDetailByElement(target);

        this._changeFieldState(targetFieldDetail, FIELD_STATES.NORMAL);

        let isAutofilled = false;
        let dimFieldDetails = [];
        for (const fieldDetail of this.fieldDetails) {
          const element = fieldDetail.elementWeakRef.get();

          if (ChromeUtils.getClassName(element) === "HTMLSelectElement") {
            // Dim fields are those we don't attempt to revert their value
            // when clear the target set, such as <select>.
            dimFieldDetails.push(fieldDetail);
          } else {
            isAutofilled |= fieldDetail.state == FIELD_STATES.AUTO_FILLED;
          }
        }
        if (!isAutofilled) {
          // Restore the dim fields to initial state as well once we knew
          // that user had intention to clear the filled form manually.
          for (const fieldDetail of dimFieldDetails) {
            this._changeFieldState(fieldDetail, FIELD_STATES.NORMAL);
          }
          this.filledRecordGUID = null;
        }
        break;
      }
    }
  }
}

class FormAutofillAddressSection extends FormAutofillSection {
  constructor(fieldDetails, winUtils) {
    super(fieldDetails, winUtils);

    this._cacheValue.oneLineStreetAddress = null;
  }

  isValidSection() {
    return this.fieldDetails.length >= FormAutofillUtils.AUTOFILL_FIELDS_THRESHOLD;
  }

  isEnabled() {
    return FormAutofill.isAutofillAddressesEnabled;
  }

  isRecordCreatable(record) {
    if (record.country && !FormAutofill.supportedCountries.includes(record.country)) {
      // We don't want to save data in the wrong fields due to not having proper
      // heuristic regexes in countries we don't yet support.
      log.warn("isRecordCreatable: Country not supported:", record.country);
      return false;
    }

    let hasName = 0;
    let length = 0;
    for (let key of Object.keys(record)) {
      if (!record[key]) {
        continue;
      }
      if (FormAutofillUtils.getCategoryFromFieldName(key) == "name") {
        hasName = 1;
        continue;
      }
      length++;
    }
    return (length + hasName) >= FormAutofillUtils.AUTOFILL_FIELDS_THRESHOLD;
  }

  _getOneLineStreetAddress(address) {
    if (!this._cacheValue.oneLineStreetAddress) {
      this._cacheValue.oneLineStreetAddress = {};
    }
    if (!this._cacheValue.oneLineStreetAddress[address]) {
      this._cacheValue.oneLineStreetAddress[address] = FormAutofillUtils.toOneLineAddress(address);
    }
    return this._cacheValue.oneLineStreetAddress[address];
  }

  addressTransformer(profile) {
    if (profile["street-address"]) {
      // "-moz-street-address-one-line" is used by the labels in
      // ProfileAutoCompleteResult.
      profile["-moz-street-address-one-line"] = this._getOneLineStreetAddress(profile["street-address"]);
      let streetAddressDetail = this.getFieldDetailByName("street-address");
      if (streetAddressDetail &&
          (ChromeUtils.getClassName(streetAddressDetail.elementWeakRef.get()) === "HTMLInputElement")) {
        profile["street-address"] = profile["-moz-street-address-one-line"];
      }

      let waitForConcat = [];
      for (let f of ["address-line3", "address-line2", "address-line1"]) {
        waitForConcat.unshift(profile[f]);
        if (this.getFieldDetailByName(f)) {
          if (waitForConcat.length > 1) {
            profile[f] = FormAutofillUtils.toOneLineAddress(waitForConcat);
          }
          waitForConcat = [];
        }
      }
    }
  }

  /**
   * Replace tel with tel-national if tel violates the input element's
   * restriction.
   * @param {Object} profile
   *        A profile to be converted.
   */
  telTransformer(profile) {
    if (!profile.tel || !profile["tel-national"]) {
      return;
    }

    let detail = this.getFieldDetailByName("tel");
    if (!detail) {
      return;
    }

    let element = detail.elementWeakRef.get();
    let _pattern;
    let testPattern = str => {
      if (!_pattern) {
        // The pattern has to match the entire value.
        _pattern = new RegExp("^(?:" + element.pattern + ")$", "u");
      }
      return _pattern.test(str);
    };
    if (element.pattern) {
      if (testPattern(profile.tel)) {
        return;
      }
    } else if (element.maxLength) {
      if (detail._reason == "autocomplete" && profile.tel.length <= element.maxLength) {
        return;
      }
    }

    if (detail._reason != "autocomplete") {
      // Since we only target people living in US and using en-US websites in
      // MVP, it makes more sense to fill `tel-national` instead of `tel`
      // if the field is identified by heuristics and no other clues to
      // determine which one is better.
      // TODO: [Bug 1407545] This should be improved once more countries are
      // supported.
      profile.tel = profile["tel-national"];
    } else if (element.pattern) {
      if (testPattern(profile["tel-national"])) {
        profile.tel = profile["tel-national"];
      }
    } else if (element.maxLength) {
      if (profile["tel-national"].length <= element.maxLength) {
        profile.tel = profile["tel-national"];
      }
    }
  }

  /*
   * Apply all address related transformers.
   *
   * @param {Object} profile
   *        A profile for adjusting address related value.
   * @override
   */
  applyTransformers(profile) {
    this.addressTransformer(profile);
    this.telTransformer(profile);
    this.matchSelectOptions(profile);
    this.adaptFieldMaxLength(profile);
  }

  computeFillingValue(value, fieldDetail, element) {
    // Try to abbreviate the value of select element.
    if (fieldDetail.fieldName == "address-level1" &&
      ChromeUtils.getClassName(element) === "HTMLSelectElement") {
      // Don't save the record when the option value is empty *OR* there
      // are multiple options being selected. The empty option is usually
      // assumed to be default along with a meaningless text to users.
      if (!value || element.selectedOptions.length != 1) {
        // Keep the property and preserve more information for address updating
        value = "";
      } else {
        let text = element.selectedOptions[0].text.trim();
        value = FormAutofillUtils.getAbbreviatedSubregionName([value, text]) || text;
      }
    }
    return value;
  }

  normalizeCreatingRecord(address) {
    if (!address) {
      return;
    }

    // Normalize Country
    if (address.record.country) {
      let detail = this.getFieldDetailByName("country");
      // Try identifying country field aggressively if it doesn't come from
      // @autocomplete.
      if (detail._reason != "autocomplete") {
        let countryCode = FormAutofillUtils.identifyCountryCode(address.record.country);
        if (countryCode) {
          address.record.country = countryCode;
        }
      }
    }

    // Normalize Tel
    FormAutofillUtils.compressTel(address.record);
    if (address.record.tel) {
      let allTelComponentsAreUntouched = Object.keys(address.record)
        .filter(field => FormAutofillUtils.getCategoryFromFieldName(field) == "tel")
        .every(field => address.untouchedFields.includes(field));
      if (allTelComponentsAreUntouched) {
        // No need to verify it if none of related fields are modified after autofilling.
        if (!address.untouchedFields.includes("tel")) {
          address.untouchedFields.push("tel");
        }
      } else {
        let strippedNumber = address.record.tel.replace(/[\s\(\)-]/g, "");

        // Remove "tel" if it contains invalid characters or the length of its
        // number part isn't between 5 and 15.
        // (The maximum length of a valid number in E.164 format is 15 digits
        //  according to https://en.wikipedia.org/wiki/E.164 )
        if (!/^(\+?)[\da-zA-Z]{5,15}$/.test(strippedNumber)) {
          address.record.tel = "";
        }
      }
    }
  }
}

class FormAutofillCreditCardSection extends FormAutofillSection {
  constructor(fieldDetails, winUtils) {
    super(fieldDetails, winUtils);
  }

  isValidSection() {
    let ccNumberReason = "";
    let hasCCNumber = false;
    let hasExpiryDate = false;
    let hasCCName = false;

    for (let detail of this.fieldDetails) {
      switch (detail.fieldName) {
        case "cc-number":
          hasCCNumber = true;
          ccNumberReason = detail._reason;
          break;
        case "cc-name":
        case "cc-given-name":
        case "cc-additional-name":
        case "cc-family-name":
          hasCCName = true;
          break;
        case "cc-exp":
        case "cc-exp-month":
        case "cc-exp-year":
          hasExpiryDate = true;
          break;
      }
    }

    return hasCCNumber && (ccNumberReason == "autocomplete" || hasExpiryDate || hasCCName);
  }

  isEnabled() {
    return FormAutofill.isAutofillCreditCardsEnabled;
  }

  isRecordCreatable(record) {
    return record["cc-number"] && FormAutofillUtils.isCCNumber(record["cc-number"]);
  }

  creditCardExpDateTransformer(profile) {
    if (!profile["cc-exp"]) {
      return;
    }

    let detail = this.getFieldDetailByName("cc-exp");
    if (!detail) {
      return;
    }

    let element = detail.elementWeakRef.get();
    if (element.tagName != "INPUT" || !element.placeholder) {
      return;
    }

    let result,
      ccExpMonth = profile["cc-exp-month"],
      ccExpYear = profile["cc-exp-year"],
      placeholder = element.placeholder;

    result = /(?:[^m]|\b)(m{1,2})\s*([-/\\]*)\s*(y{2,4})(?!y)/i.exec(placeholder);
    if (result) {
      profile["cc-exp"] = String(ccExpMonth).padStart(result[1].length, "0") +
                          result[2] +
                          String(ccExpYear).substr(-1 * result[3].length);
      return;
    }

    result = /(?:[^y]|\b)(y{2,4})\s*([-/\\]*)\s*(m{1,2})(?!m)/i.exec(placeholder);
    if (result) {
      profile["cc-exp"] = String(ccExpYear).substr(-1 * result[1].length) +
                          result[2] +
                          String(ccExpMonth).padStart(result[3].length, "0");
    }
  }

  async _decrypt(cipherText, reauth) {
    return new Promise((resolve) => {
      Services.cpmm.addMessageListener("FormAutofill:DecryptedString", function getResult(result) {
        Services.cpmm.removeMessageListener("FormAutofill:DecryptedString", getResult);
        resolve(result.data);
      });

      Services.cpmm.sendAsyncMessage("FormAutofill:GetDecryptedString", {cipherText, reauth});
    });
  }

  /*
   * Apply all credit card related transformers.
   *
   * @param {Object} profile
   *        A profile for adjusting credit card related value.
   * @override
   */
  applyTransformers(profile) {
    this.matchSelectOptions(profile);
    this.creditCardExpDateTransformer(profile);
    this.adaptFieldMaxLength(profile);
  }

  /**
   * Customize for previewing prorifle.
   *
   * @param {Object} profile
   *        A profile for pre-processing before previewing values.
   * @override
   */
  preparePreviewProfile(profile) {
    // Always show the decrypted credit card number when Master Password is
    // disabled.
    if (profile["cc-number-decrypted"]) {
      profile["cc-number"] = profile["cc-number-decrypted"];
    }
  }

  /**
   * Customize for filling prorifle.
   *
   * @param {Object} profile
   *        A profile for pre-processing before filling values.
   * @returns {boolean} Whether the profile should be filled.
   * @override
   */
  async prepareFillingProfile(profile) {
    // Prompt the OS login dialog to get the decrypted credit
    // card number.
    if (profile["cc-number-encrypted"]) {
      let decrypted = await this._decrypt(profile["cc-number-encrypted"], reauthPasswordPromptMessage);

      if (!decrypted) {
        // Early return if the decrypted is empty or undefined
        return false;
      }

      profile["cc-number"] = decrypted;
    }
    return true;
  }
}

/**
 * Handles profile autofill for a DOM Form element.
 */
class FormAutofillHandler {
  /**
   * Initialize the form from `FormLike` object to handle the section or form
   * operations.
   * @param {FormLike} form Form that need to be auto filled
   */
  constructor(form) {
    this._updateForm(form);

    /**
     * A WindowUtils reference of which Window the form belongs
     */
    this.winUtils = this.form.rootElement.ownerGlobal.windowUtils;

    /**
     * Time in milliseconds since epoch when a user started filling in the form.
     */
    this.timeStartedFillingMS = null;
  }

  set focusedInput(element) {
    let section = this._sectionCache.get(element);
    if (!section) {
      section = this.sections.find(
        s => s.getFieldDetailByElement(element)
      );
      this._sectionCache.set(element, section);
    }

    this._focusedSection = section;

    if (section) {
      section.focusedInput = element;
    }
  }

  get activeSection() {
    return this._focusedSection;
  }

  /**
   * Check the form is necessary to be updated. This function should be able to
   * detect any changes including all control elements in the form.
   * @param {HTMLElement} element The element supposed to be in the form.
   * @returns {boolean} FormAutofillHandler.form is updated or not.
   */
  updateFormIfNeeded(element) {
    // When the following condition happens, FormAutofillHandler.form should be
    // updated:
    // * The count of form controls is changed.
    // * When the element can not be found in the current form.
    //
    // However, we should improve the function to detect the element changes.
    // e.g. a tel field is changed from type="hidden" to type="tel".

    let _formLike;
    let getFormLike = () => {
      if (!_formLike) {
        _formLike = FormLikeFactory.createFromField(element);
      }
      return _formLike;
    };

    let currentForm = element.form;
    if (!currentForm) {
      currentForm = getFormLike();
    }

    if (currentForm.elements.length != this.form.elements.length) {
      log.debug("The count of form elements is changed.");
      this._updateForm(getFormLike());
      return true;
    }

    if (!this.form.elements.includes(element)) {
      log.debug("The element can not be found in the current form.");
      this._updateForm(getFormLike());
      return true;
    }

    return false;
  }

  /**
   * Update the form with a new FormLike, and the related fields should be
   * updated or clear to ensure the data consistency.
   * @param {FormLike} form a new FormLike to replace the original one.
   */
  _updateForm(form) {
    /**
     * DOM Form element to which this object is attached.
     */
    this.form = form;

    /**
     * Array of collected data about relevant form fields.  Each item is an object
     * storing the identifying details of the field and a reference to the
     * originally associated element from the form.
     *
     * The "section", "addressType", "contactType", and "fieldName" values are
     * used to identify the exact field when the serializable data is received
     * from the backend.  There cannot be multiple fields which have
     * the same exact combination of these values.
     *
     * A direct reference to the associated element cannot be sent to the user
     * interface because processing may be done in the parent process.
     */
    this.fieldDetails = null;

    this.sections = [];
    this._sectionCache = new WeakMap();
  }

  /**
   * Set fieldDetails from the form about fields that can be autofilled.
   *
   * @param {boolean} allowDuplicates
   *        true to remain any duplicated field details otherwise to remove the
   *        duplicated ones.
   * @returns {Array} The valid address and credit card details.
   */
  collectFormFields(allowDuplicates = false) {
    let sections = FormAutofillHeuristics.getFormInfo(this.form, allowDuplicates);
    let allValidDetails = [];
    for (let {fieldDetails, type} of sections) {
      let section;
      if (type == FormAutofillUtils.SECTION_TYPES.ADDRESS) {
        section = new FormAutofillAddressSection(fieldDetails, this.winUtils);
      } else if (type == FormAutofillUtils.SECTION_TYPES.CREDIT_CARD) {
        section = new FormAutofillCreditCardSection(fieldDetails, this.winUtils);
      } else {
        throw new Error("Unknown field type.");
      }
      this.sections.push(section);
      allValidDetails.push(...section.fieldDetails);
    }

    for (let detail of allValidDetails) {
      let input = detail.elementWeakRef.get();
      if (!input) {
        continue;
      }
      input.addEventListener("input", this, {mozSystemGroup: true});
    }

    this.fieldDetails = allValidDetails;
    return allValidDetails;
  }

  _hasFilledSection() {
    return this.sections.some(section => section.isFilled());
  }

  /**
   * Processes form fields that can be autofilled, and populates them with the
   * profile provided by backend.
   *
   * @param {Object} profile
   *        A profile to be filled in.
   */
  async autofillFormFields(profile) {
    let noFilledSectionsPreviously = !this._hasFilledSection();
    await this.activeSection.autofillFields(profile);

    const onChangeHandler = e => {
      if (!e.isTrusted) {
        return;
      }
      if (e.type == "reset") {
        for (let section of this.sections) {
          section.resetFieldStates();
        }
      }
      // Unregister listeners once no field is in AUTO_FILLED state.
      if (!this._hasFilledSection()) {
        this.form.rootElement.removeEventListener("input", onChangeHandler, {mozSystemGroup: true});
        this.form.rootElement.removeEventListener("reset", onChangeHandler, {mozSystemGroup: true});
      }
    };

    if (noFilledSectionsPreviously) {
      // Handle the highlight style resetting caused by user's correction afterward.
      log.debug("register change handler for filled form:", this.form);
      this.form.rootElement.addEventListener("input", onChangeHandler, {mozSystemGroup: true});
      this.form.rootElement.addEventListener("reset", onChangeHandler, {mozSystemGroup: true});
    }
  }

  handleEvent(event) {
    switch (event.type) {
      case "input":
        if (!event.isTrusted) {
          return;
        }

        for (let detail of this.fieldDetails) {
          let input = detail.elementWeakRef.get();
          if (!input) {
            continue;
          }
          input.removeEventListener("input", this, {mozSystemGroup: true});
        }
        this.timeStartedFillingMS = Date.now();
        break;
    }
  }

  /**
   * Collect the filled sections within submitted form and convert all the valid
   * field data into multiple records.
   *
   * @returns {Object} records
   *          {Array.<Object>} records.address
   *          {Array.<Object>} records.creditCard
   */
  createRecords() {
    const records = {
      address: [],
      creditCard: [],
    };

    for (const section of this.sections) {
      const secRecord = section.createRecord();
      if (!secRecord) {
        continue;
      }
      if (section instanceof FormAutofillAddressSection) {
        records.address.push(secRecord);
      } else if (section instanceof FormAutofillCreditCardSection) {
        records.creditCard.push(secRecord);
      } else {
        throw new Error("Unknown section type");
      }
    }
    log.debug("Create records:", records);
    return records;
  }
}
PK
!<7Bö%chrome/res/FormAutofillHeuristics.jsm/* 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/. */

/*
 * Form Autofill field heuristics.
 */

"use strict";

var EXPORTED_SYMBOLS = ["FormAutofillHeuristics", "LabelUtils"];

ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.import("resource://formautofill/FormAutofill.jsm");
ChromeUtils.defineModuleGetter(this, "FormAutofillUtils",
                               "resource://formautofill/FormAutofillUtils.jsm");

this.log = null;
FormAutofill.defineLazyLogGetter(this, EXPORTED_SYMBOLS[0]);

const PREF_HEURISTICS_ENABLED = "extensions.formautofill.heuristics.enabled";
const PREF_SECTION_ENABLED = "extensions.formautofill.section.enabled";
const DEFAULT_SECTION_NAME = "-moz-section-default";

/**
 * A scanner for traversing all elements in a form and retrieving the field
 * detail with FormAutofillHeuristics.getInfo function. It also provides a
 * cursor (parsingIndex) to indicate which element is waiting for parsing.
 */
class FieldScanner {
  /**
   * Create a FieldScanner based on form elements with the existing
   * fieldDetails.
   *
   * @param {Array.DOMElement} elements
   *        The elements from a form for each parser.
   */
  constructor(elements, {allowDuplicates = false, sectionEnabled = true}) {
    this._elementsWeakRef = Cu.getWeakReference(elements);
    this.fieldDetails = [];
    this._parsingIndex = 0;
    this._sections = [];
    this._allowDuplicates = allowDuplicates;
    this._sectionEnabled = sectionEnabled;
  }

  get _elements() {
    return this._elementsWeakRef.get();
  }

  /**
   * This cursor means the index of the element which is waiting for parsing.
   *
   * @returns {number}
   *          The index of the element which is waiting for parsing.
   */
  get parsingIndex() {
    return this._parsingIndex;
  }

  /**
   * Move the parsingIndex to the next elements. Any elements behind this index
   * means the parsing tasks are finished.
   *
   * @param {number} index
   *        The latest index of elements waiting for parsing.
   */
  set parsingIndex(index) {
    if (index > this._elements.length) {
      throw new Error("The parsing index is out of range.");
    }
    this._parsingIndex = index;
  }

  /**
   * Retrieve the field detail by the index. If the field detail is not ready,
   * the elements will be traversed until matching the index.
   *
   * @param {number} index
   *        The index of the element that you want to retrieve.
   * @returns {Object}
   *          The field detail at the specific index.
   */
  getFieldDetailByIndex(index) {
    if (index >= this._elements.length) {
      throw new Error(`The index ${index} is out of range.(${this._elements.length})`);
    }

    if (index < this.fieldDetails.length) {
      return this.fieldDetails[index];
    }

    for (let i = this.fieldDetails.length; i < (index + 1); i++) {
      this.pushDetail();
    }

    return this.fieldDetails[index];
  }

  get parsingFinished() {
    return this.parsingIndex >= this._elements.length;
  }

  _pushToSection(name, fieldDetail) {
    for (let section of this._sections) {
      if (section.name == name) {
        section.fieldDetails.push(fieldDetail);
        return;
      }
    }
    this._sections.push({
      name,
      fieldDetails: [fieldDetail],
    });
  }

  _classifySections() {
    let fieldDetails = this._sections[0].fieldDetails;
    this._sections = [];
    let seenTypes = new Set();
    let previousType;
    let sectionCount = 0;

    for (let fieldDetail of fieldDetails) {
      if (!fieldDetail.fieldName) {
        continue;
      }
      if (seenTypes.has(fieldDetail.fieldName) &&
          previousType != fieldDetail.fieldName) {
        seenTypes.clear();
        sectionCount++;
      }
      previousType = fieldDetail.fieldName;
      seenTypes.add(fieldDetail.fieldName);
      this._pushToSection(DEFAULT_SECTION_NAME + "-" + sectionCount, fieldDetail);
    }
  }

  /**
   * The result is an array contains the sections with its belonging field
   * details. If `this._sections` contains one section only with the default
   * section name (DEFAULT_SECTION_NAME), `this._classifySections` should be
   * able to identify all sections in the heuristic way.
   *
   * @returns {Array<Object>}
   *          The array with the sections, and the belonging fieldDetails are in
   *          each section.
   */
  getSectionFieldDetails() {
    // When the section feature is disabled, `getSectionFieldDetails` should
    // provide a single address and credit card section result.
    if (!this._sectionEnabled) {
      return this._getFinalDetails(this.fieldDetails);
    }
    if (this._sections.length == 0) {
      return [];
    }
    if (this._sections.length == 1 && this._sections[0].name == DEFAULT_SECTION_NAME) {
      this._classifySections();
    }

    return this._sections.reduce((sections, current) => {
      sections.push(...this._getFinalDetails(current.fieldDetails));
      return sections;
    }, []);
  }

  /**
   * This function will prepare an autocomplete info object with getInfo
   * function and push the detail to fieldDetails property.
   * Any field will be pushed into `this._sections` based on the section name
   * in `autocomplete` attribute.
   *
   * Any element without the related detail will be used for adding the detail
   * to the end of field details.
   */
  pushDetail() {
    let elementIndex = this.fieldDetails.length;
    if (elementIndex >= this._elements.length) {
      throw new Error("Try to push the non-existing element info.");
    }
    let element = this._elements[elementIndex];
    let info = FormAutofillHeuristics.getInfo(element);
    let fieldInfo = {
      section: info ? info.section : "",
      addressType: info ? info.addressType : "",
      contactType: info ? info.contactType : "",
      fieldName: info ? info.fieldName : "",
      elementWeakRef: Cu.getWeakReference(element),
    };

    if (info && info._reason) {
      fieldInfo._reason = info._reason;
    }

    this.fieldDetails.push(fieldInfo);
    this._pushToSection(this._getSectionName(fieldInfo), fieldInfo);
  }

  _getSectionName(info) {
    let names = [];
    if (info.section) {
      names.push(info.section);
    }
    if (info.addressType) {
      names.push(info.addressType);
    }
    return names.length ? names.join(" ") : DEFAULT_SECTION_NAME;
  }

  /**
   * When a field detail should be changed its fieldName after parsing, use
   * this function to update the fieldName which is at a specific index.
   *
   * @param {number} index
   *        The index indicates a field detail to be updated.
   * @param {string} fieldName
   *        The new fieldName
   */
  updateFieldName(index, fieldName) {
    if (index >= this.fieldDetails.length) {
      throw new Error("Try to update the non-existing field detail.");
    }
    this.fieldDetails[index].fieldName = fieldName;
  }

  _isSameField(field1, field2) {
    return field1.section == field2.section &&
           field1.addressType == field2.addressType &&
           field1.fieldName == field2.fieldName;
  }

  /**
   * Provide the final field details without invalid field name, and the
   * duplicated fields will be removed as well. For the debugging purpose,
   * the final `fieldDetails` will include the duplicated fields if
   * `_allowDuplicates` is true.
   *
   * Each item should contain one type of fields only, and the two valid types
   * are Address and CreditCard.
   *
   * @param   {Array<Object>} fieldDetails
   *          The field details for trimming.
   * @returns {Array<Object>}
   *          The array with the field details without invalid field name and
   *          duplicated fields.
   */
  _getFinalDetails(fieldDetails) {
    let addressFieldDetails = [];
    let creditCardFieldDetails = [];
    for (let fieldDetail of fieldDetails) {
      let fieldName = fieldDetail.fieldName;
      if (FormAutofillUtils.isAddressField(fieldName)) {
        addressFieldDetails.push(fieldDetail);
      } else if (FormAutofillUtils.isCreditCardField(fieldName)) {
        creditCardFieldDetails.push(fieldDetail);
      } else {
        log.debug("Not collecting a field with a unknown fieldName", fieldDetail);
      }
    }

    return [
      {
        type: FormAutofillUtils.SECTION_TYPES.ADDRESS,
        fieldDetails: addressFieldDetails,
      },
      {
        type: FormAutofillUtils.SECTION_TYPES.CREDIT_CARD,
        fieldDetails: creditCardFieldDetails,
      },
    ].map(section => {
      if (this._allowDuplicates) {
        return section;
      }
      // Deduplicate each set of fieldDetails
      let details = section.fieldDetails;
      section.fieldDetails = details.filter((detail, index) => {
        let previousFields = details.slice(0, index);
        return !previousFields.find(f => this._isSameField(detail, f));
      });
      return section;
    }).filter(section => section.fieldDetails.length > 0);
  }

  elementExisting(index) {
    return index < this._elements.length;
  }
}

var LabelUtils = {
  // The tag name list is from Chromium except for "STYLE":
  // eslint-disable-next-line max-len
  // https://cs.chromium.org/chromium/src/components/autofill/content/renderer/form_autofill_util.cc?l=216&rcl=d33a171b7c308a64dc3372fac3da2179c63b419e
  EXCLUDED_TAGS: ["SCRIPT", "NOSCRIPT", "OPTION", "STYLE"],

  // A map object, whose keys are the id's of form fields and each value is an
  // array consisting of label elements correponding to the id.
  // @type {Map<string, array>}
  _mappedLabels: null,

  // An array consisting of label elements whose correponding form field doesn't
  // have an id attribute.
  // @type {Array<HTMLLabelElement>}
  _unmappedLabels: null,

  // A weak map consisting of label element and extracted strings pairs.
  // @type {WeakMap<HTMLLabelElement, array>}
  _labelStrings: null,

  /**
   * Extract all strings of an element's children to an array.
   * "element.textContent" is a string which is merged of all children nodes,
   * and this function provides an array of the strings contains in an element.
   *
   * @param  {Object} element
   *         A DOM element to be extracted.
   * @returns {Array}
   *          All strings in an element.
   */
  extractLabelStrings(element) {
    if (this._labelStrings.has(element)) {
      return this._labelStrings.get(element);
    }
    let strings = [];
    let _extractLabelStrings = (el) => {
      if (this.EXCLUDED_TAGS.includes(el.tagName)) {
        return;
      }

      if (el.nodeType == el.TEXT_NODE || el.childNodes.length == 0) {
        let trimmedText = el.textContent.trim();
        if (trimmedText) {
          strings.push(trimmedText);
        }
        return;
      }

      for (let node of el.childNodes) {
        let nodeType = node.nodeType;
        if (nodeType != node.ELEMENT_NODE && nodeType != node.TEXT_NODE) {
          continue;
        }
        _extractLabelStrings(node);
      }
    };
    _extractLabelStrings(element);
    this._labelStrings.set(element, strings);
    return strings;
  },

  generateLabelMap(doc) {
    let mappedLabels = new Map();
    let unmappedLabels = [];

    for (let label of doc.querySelectorAll("label")) {
      let id = label.htmlFor;
      if (!id) {
        let control = label.control;
        if (!control) {
          continue;
        }
        id = control.id;
      }
      if (id) {
        let labels = mappedLabels.get(id);
        if (labels) {
          labels.push(label);
        } else {
          mappedLabels.set(id, [label]);
        }
      } else {
        unmappedLabels.push(label);
      }
    }

    this._mappedLabels = mappedLabels;
    this._unmappedLabels = unmappedLabels;
    this._labelStrings = new WeakMap();
  },

  clearLabelMap() {
    this._mappedLabels = null;
    this._unmappedLabels = null;
    this._labelStrings = null;
  },

  findLabelElements(element) {
    if (!this._mappedLabels) {
      this.generateLabelMap(element.ownerDocument);
    }

    let id = element.id;
    if (!id) {
      return this._unmappedLabels.filter(label => label.control == element);
    }
    return this._mappedLabels.get(id) || [];
  },
};

/**
 * Returns the autocomplete information of fields according to heuristics.
 */
this.FormAutofillHeuristics = {
  RULES: null,

  /**
   * Try to find a contiguous sub-array within an array.
   *
   * @param {Array} array
   * @param {Array} subArray
   *
   * @returns {boolean}
   *          Return whether subArray was found within the array or not.
   */
  _matchContiguousSubArray(array, subArray) {
    return array.some((elm, i) => subArray.every((sElem, j) => sElem == array[i + j]));
  },

  /**
   * Try to find the field that is look like a month select.
   *
   * @param {DOMElement} element
   * @returns {boolean}
   *          Return true if we observe the trait of month select in
   *          the current element.
   */
  _isExpirationMonthLikely(element) {
    if (ChromeUtils.getClassName(element) !== "HTMLSelectElement") {
      return false;
    }

    const options = [...element.options];
    const desiredValues = Array(12).fill(1).map((v, i) => v + i);

    // The number of month options shouldn't be less than 12 or larger than 13
    // including the default option.
    if (options.length < 12 || options.length > 13) {
      return false;
    }

    return this._matchContiguousSubArray(options.map(e => +e.value), desiredValues) ||
           this._matchContiguousSubArray(options.map(e => +e.label), desiredValues);
  },


  /**
   * Try to find the field that is look like a year select.
   *
   * @param {DOMElement} element
   * @returns {boolean}
   *          Return true if we observe the trait of year select in
   *          the current element.
   */
  _isExpirationYearLikely(element) {
    if (ChromeUtils.getClassName(element) !== "HTMLSelectElement") {
      return false;
    }

    const options = [...element.options];
    // A normal expiration year select should contain at least the last three years
    // in the list.
    const curYear = new Date().getFullYear();
    const desiredValues = Array(3).fill(0).map((v, i) => v + curYear + i);

    return this._matchContiguousSubArray(options.map(e => +e.value), desiredValues) ||
           this._matchContiguousSubArray(options.map(e => +e.label), desiredValues);
  },


  /**
   * Try to match the telephone related fields to the grammar
   * list to see if there is any valid telephone set and correct their
   * field names.
   *
   * @param {FieldScanner} fieldScanner
   *        The current parsing status for all elements
   * @returns {boolean}
   *          Return true if there is any field can be recognized in the parser,
   *          otherwise false.
   */
  _parsePhoneFields(fieldScanner) {
    let matchingResult;

    const GRAMMARS = this.PHONE_FIELD_GRAMMARS;
    for (let i = 0; i < GRAMMARS.length; i++) {
      let detailStart = fieldScanner.parsingIndex;
      let ruleStart = i;
      for (; i < GRAMMARS.length && GRAMMARS[i][0] && fieldScanner.elementExisting(detailStart); i++, detailStart++) {
        let detail = fieldScanner.getFieldDetailByIndex(detailStart);
        if (!detail || GRAMMARS[i][0] != detail.fieldName || (detail._reason && detail._reason == "autocomplete")) {
          break;
        }
        let element = detail.elementWeakRef.get();
        if (!element) {
          break;
        }
        if (GRAMMARS[i][2] && (!element.maxLength || GRAMMARS[i][2] < element.maxLength)) {
          break;
        }
      }
      if (i >= GRAMMARS.length) {
        break;
      }

      if (!GRAMMARS[i][0]) {
        matchingResult = {
          ruleFrom: ruleStart,
          ruleTo: i,
        };
        break;
      }

      // Fast rewinding to the next rule.
      for (; i < GRAMMARS.length; i++) {
        if (!GRAMMARS[i][0]) {
          break;
        }
      }
    }

    let parsedField = false;
    if (matchingResult) {
      let {ruleFrom, ruleTo} = matchingResult;
      let detailStart = fieldScanner.parsingIndex;
      for (let i = ruleFrom; i < ruleTo; i++) {
        fieldScanner.updateFieldName(detailStart, GRAMMARS[i][1]);
        fieldScanner.parsingIndex++;
        detailStart++;
        parsedField = true;
      }
    }

    if (fieldScanner.parsingFinished) {
      return parsedField;
    }

    let nextField = fieldScanner.getFieldDetailByIndex(fieldScanner.parsingIndex);
    if (nextField && nextField._reason != "autocomplete" && fieldScanner.parsingIndex > 0) {
      const regExpTelExtension = new RegExp(
        "\\bext|ext\\b|extension" +
        "|ramal", // pt-BR, pt-PT
        "iu");
      const previousField = fieldScanner.getFieldDetailByIndex(fieldScanner.parsingIndex - 1);
      const previousFieldType = FormAutofillUtils.getCategoryFromFieldName(previousField.fieldName);
      if (previousField && previousFieldType == "tel" &&
        this._matchRegexp(nextField.elementWeakRef.get(), regExpTelExtension)) {
        fieldScanner.updateFieldName(fieldScanner.parsingIndex, "tel-extension");
        fieldScanner.parsingIndex++;
        parsedField = true;
      }
    }

    return parsedField;
  },

  /**
   * Try to find the correct address-line[1-3] sequence and correct their field
   * names.
   *
   * @param {FieldScanner} fieldScanner
   *        The current parsing status for all elements
   * @returns {boolean}
   *          Return true if there is any field can be recognized in the parser,
   *          otherwise false.
   */
  _parseAddressFields(fieldScanner) {
    let parsedFields = false;
    const addressLines = ["address-line1", "address-line2", "address-line3"];

    // TODO: These address-line* regexps are for the lines with numbers, and
    // they are the subset of the regexps in `heuristicsRegexp.js`. We have to
    // find a better way to make them consistent.
    const addressLineRegexps = {
      "address-line1": new RegExp(
        "address[_-]?line(1|one)|address1|addr1" +
        "|addrline1|address_1" + // Extra rules by Firefox
        "|indirizzo1" + // it-IT
        "|住所1" + // ja-JP
        "|地址1" + // zh-CN
        "|주소.?1", // ko-KR
        "iu"
      ),
      "address-line2": new RegExp(
        "address[_-]?line(2|two)|address2|addr2" +
        "|addrline2|address_2" + // Extra rules by Firefox
        "|indirizzo2" + // it-IT
        "|住所2" + // ja-JP
        "|地址2" + // zh-CN
        "|주소.?2", // ko-KR
        "iu"
      ),
      "address-line3": new RegExp(
        "address[_-]?line(3|three)|address3|addr3" +
        "|addrline3|address_3" + // Extra rules by Firefox
        "|indirizzo3" + // it-IT
        "|住所3" + // ja-JP
        "|地址3" + // zh-CN
        "|주소.?3", // ko-KR
        "iu"
      ),
    };
    while (!fieldScanner.parsingFinished) {
      let detail = fieldScanner.getFieldDetailByIndex(fieldScanner.parsingIndex);
      if (!detail || !addressLines.includes(detail.fieldName) || detail._reason == "autocomplete") {
        // When the field is not related to any address-line[1-3] fields or
        // determined by autocomplete attr, it means the parsing process can be
        // terminated.
        break;
      }
      const elem = detail.elementWeakRef.get();
      for (let regexp of Object.keys(addressLineRegexps)) {
        if (this._matchRegexp(elem, addressLineRegexps[regexp])) {
          fieldScanner.updateFieldName(fieldScanner.parsingIndex, regexp);
          parsedFields = true;
        }
      }
      fieldScanner.parsingIndex++;
    }

    return parsedFields;
  },

  /**
   * Try to look for expiration date fields and revise the field names if needed.
   *
   * @param {FieldScanner} fieldScanner
   *        The current parsing status for all elements
   * @returns {boolean}
   *          Return true if there is any field can be recognized in the parser,
   *          otherwise false.
   */
  _parseCreditCardExpirationDateFields(fieldScanner) {
    if (fieldScanner.parsingFinished) {
      return false;
    }

    const savedIndex = fieldScanner.parsingIndex;
    const monthAndYearFieldNames = ["cc-exp-month", "cc-exp-year"];
    const detail = fieldScanner.getFieldDetailByIndex(fieldScanner.parsingIndex);
    const element = detail.elementWeakRef.get();

    // Respect to autocomplete attr and skip the uninteresting fields
    if (!detail || (detail._reason && detail._reason == "autocomplete") ||
        !["cc-exp", ...monthAndYearFieldNames].includes(detail.fieldName)) {
      return false;
    }

    // If the input type is a month picker, then assume it's cc-exp.
    if (element.type == "month") {
      fieldScanner.updateFieldName(fieldScanner.parsingIndex, "cc-exp");
      fieldScanner.parsingIndex++;

      return true;
    }

    // Don't process the fields if expiration month and expiration year are already
    // matched by regex in correct order.
    if (fieldScanner.getFieldDetailByIndex(fieldScanner.parsingIndex++).fieldName == "cc-exp-month" &&
        !fieldScanner.parsingFinished &&
        fieldScanner.getFieldDetailByIndex(fieldScanner.parsingIndex++).fieldName == "cc-exp-year") {
      return true;
    }
    fieldScanner.parsingIndex = savedIndex;

    // Determine the field name by checking if the fields are month select and year select
    // likely.
    if (this._isExpirationMonthLikely(element)) {
      fieldScanner.updateFieldName(fieldScanner.parsingIndex, "cc-exp-month");
      fieldScanner.parsingIndex++;
      if (!fieldScanner.parsingFinished) {
        const nextDetail = fieldScanner.getFieldDetailByIndex(fieldScanner.parsingIndex);
        const nextElement = nextDetail.elementWeakRef.get();
        if (this._isExpirationYearLikely(nextElement)) {
          fieldScanner.updateFieldName(fieldScanner.parsingIndex, "cc-exp-year");
          fieldScanner.parsingIndex++;
          return true;
        }
      }
    }
    fieldScanner.parsingIndex = savedIndex;

    // Verify that the following consecutive two fields can match cc-exp-month and cc-exp-year
    // respectively.
    if (this._findMatchedFieldName(element, ["cc-exp-month"])) {
      fieldScanner.updateFieldName(fieldScanner.parsingIndex, "cc-exp-month");
      fieldScanner.parsingIndex++;
      if (!fieldScanner.parsingFinished) {
        const nextDetail = fieldScanner.getFieldDetailByIndex(fieldScanner.parsingIndex);
        const nextElement = nextDetail.elementWeakRef.get();
        if (this._findMatchedFieldName(nextElement, ["cc-exp-year"])) {
          fieldScanner.updateFieldName(fieldScanner.parsingIndex, "cc-exp-year");
          fieldScanner.parsingIndex++;
          return true;
        }
      }
    }
    fieldScanner.parsingIndex = savedIndex;

    // Look for MM and/or YY(YY).
    if (this._matchRegexp(element, /^mm$/ig)) {
      fieldScanner.updateFieldName(fieldScanner.parsingIndex, "cc-exp-month");
      fieldScanner.parsingIndex++;
      if (!fieldScanner.parsingFinished) {
        const nextDetail = fieldScanner.getFieldDetailByIndex(fieldScanner.parsingIndex);
        const nextElement = nextDetail.elementWeakRef.get();
        if (this._matchRegexp(nextElement, /^(yy|yyyy)$/)) {
          fieldScanner.updateFieldName(fieldScanner.parsingIndex, "cc-exp-year");
          fieldScanner.parsingIndex++;

          return true;
        }
      }
    }
    fieldScanner.parsingIndex = savedIndex;

    // Look for a cc-exp with 2-digit or 4-digit year.
    if (this._matchRegexp(element, /(?:exp.*date[^y\\n\\r]*|mm\\s*[-/]?\\s*)yy(?:[^y]|$)/ig) ||
        this._matchRegexp(element, /(?:exp.*date[^y\\n\\r]*|mm\\s*[-/]?\\s*)yyyy(?:[^y]|$)/ig)) {
      fieldScanner.updateFieldName(fieldScanner.parsingIndex, "cc-exp");
      fieldScanner.parsingIndex++;
      return true;
    }
    fieldScanner.parsingIndex = savedIndex;

    // Match general cc-exp regexp at last.
    if (this._findMatchedFieldName(element, ["cc-exp"])) {
      fieldScanner.updateFieldName(fieldScanner.parsingIndex, "cc-exp");
      fieldScanner.parsingIndex++;
      return true;
    }
    fieldScanner.parsingIndex = savedIndex;

    // Set current field name to null as it failed to match any patterns.
    fieldScanner.updateFieldName(fieldScanner.parsingIndex, null);
    fieldScanner.parsingIndex++;
    return true;
  },

  /**
   * This function should provide all field details of a form which are placed
   * in the belonging section. The details contain the autocomplete info
   * (e.g. fieldName, section, etc).
   *
   * `allowDuplicates` is used for the xpcshell-test purpose currently because
   * the heuristics should be verified that some duplicated elements still can
   * be predicted correctly.
   *
   * @param {HTMLFormElement} form
   *        the elements in this form to be predicted the field info.
   * @param {boolean} allowDuplicates
   *        true to remain any duplicated field details otherwise to remove the
   *        duplicated ones.
   * @returns {Array<Array<Object>>}
   *        all sections within its field details in the form.
   */
  getFormInfo(form, allowDuplicates = false) {
    const eligibleFields = Array.from(form.elements)
      .filter(elem => FormAutofillUtils.isFieldEligibleForAutofill(elem));

    if (eligibleFields.length <= 0) {
      return [];
    }

    let fieldScanner = new FieldScanner(eligibleFields,
      {allowDuplicates, sectionEnabled: this._sectionEnabled});
    while (!fieldScanner.parsingFinished) {
      let parsedPhoneFields = this._parsePhoneFields(fieldScanner);
      let parsedAddressFields = this._parseAddressFields(fieldScanner);
      let parsedExpirationDateFields = this._parseCreditCardExpirationDateFields(fieldScanner);

      // If there is no any field parsed, the parsing cursor can be moved
      // forward to the next one.
      if (!parsedPhoneFields && !parsedAddressFields && !parsedExpirationDateFields) {
        fieldScanner.parsingIndex++;
      }
    }

    LabelUtils.clearLabelMap();

    return fieldScanner.getSectionFieldDetails();
  },

  _regExpTableHashValue(...signBits) {
    return signBits.reduce((p, c, i) => p | !!c << i, 0);
  },

  _setRegExpListCache(regexps, b0, b1, b2) {
    if (!this._regexpList) {
      this._regexpList = [];
    }
    this._regexpList[this._regExpTableHashValue(b0, b1, b2)] = regexps;
  },

  _getRegExpListCache(b0, b1, b2) {
    if (!this._regexpList) {
      return null;
    }
    return this._regexpList[this._regExpTableHashValue(b0, b1, b2)] || null;
  },

  _getRegExpList(isAutoCompleteOff, elementTagName) {
    let isSelectElem = elementTagName == "SELECT";
    let regExpListCache = this._getRegExpListCache(
      isAutoCompleteOff,
      FormAutofill.isAutofillCreditCardsAvailable,
      isSelectElem
    );
    if (regExpListCache) {
      return regExpListCache;
    }
    const FIELDNAMES_IGNORING_AUTOCOMPLETE_OFF = [
      "cc-name",
      "cc-number",
      "cc-exp-month",
      "cc-exp-year",
      "cc-exp",
    ];
    let regexps = isAutoCompleteOff ? FIELDNAMES_IGNORING_AUTOCOMPLETE_OFF : Object.keys(this.RULES);

    if (!FormAutofill.isAutofillCreditCardsAvailable) {
      regexps = regexps.filter(name => !FormAutofillUtils.isCreditCardField(name));
    }

    if (isSelectElem) {
      const FIELDNAMES_FOR_SELECT_ELEMENT = [
        "address-level1",
        "address-level2",
        "country",
        "cc-exp-month",
        "cc-exp-year",
        "cc-exp",
      ];
      regexps = regexps.filter(name => FIELDNAMES_FOR_SELECT_ELEMENT.includes(name));
    }

    this._setRegExpListCache(
      regexps,
      isAutoCompleteOff,
      FormAutofill.isAutofillCreditCardsAvailable,
      isSelectElem
    );

    return regexps;
  },

  getInfo(element) {
    let info = element.getAutocompleteInfo();
    // An input[autocomplete="on"] will not be early return here since it stll
    // needs to find the field name.
    if (info && info.fieldName && info.fieldName != "on" && info.fieldName != "off") {
      info._reason = "autocomplete";
      return info;
    }

    if (!this._prefEnabled) {
      return null;
    }

    let isAutoCompleteOff = element.autocomplete == "off" ||
      (element.form && element.form.autocomplete == "off");

    // "email" type of input is accurate for heuristics to determine its Email
    // field or not. However, "tel" type is used for ZIP code for some web site
    // (e.g. HomeDepot, BestBuy), so "tel" type should be not used for "tel"
    // prediction.
    if (element.type == "email" && !isAutoCompleteOff) {
      return {
        fieldName: "email",
        section: "",
        addressType: "",
        contactType: "",
      };
    }

    let regexps = this._getRegExpList(isAutoCompleteOff, element.tagName);
    if (regexps.length == 0) {
      return null;
    }

    let matchedFieldName =  this._findMatchedFieldName(element, regexps);
    if (matchedFieldName) {
      return {
        fieldName: matchedFieldName,
        section: "",
        addressType: "",
        contactType: "",
      };
    }

    return null;
  },

  /**
   * @typedef ElementStrings
   * @type {object}
   * @yield {string} id - element id.
   * @yield {string} name - element name.
   * @yield {Array<string>} labels - extracted labels.
   */

  /**
   * Extract all the signature strings of an element.
   *
   * @param {HTMLElement} element
   * @returns {ElementStrings}
   */
  _getElementStrings(element) {
    return {
      * [Symbol.iterator]() {
        yield element.id;
        yield element.name;

        const labels = LabelUtils.findLabelElements(element);
        for (let label of labels) {
          yield *LabelUtils.extractLabelStrings(label);
        }
      },
    };
  },

  /**
   * Find the first matched field name of the element wih given regex list.
   *
   * @param {HTMLElement} element
   * @param {Array<string>} regexps
   *        The regex key names that correspond to pattern in the rule list.
   * @returns {?string} The first matched field name
   */
  _findMatchedFieldName(element, regexps) {
    const getElementStrings = this._getElementStrings(element);
    for (let regexp of regexps) {
      for (let string of getElementStrings) {
        // The original regexp "(?<!united )state|county|region|province" for
        // "address-line1" wants to exclude any "united state" string, so the
        // following code is to remove all "united state" string before applying
        // "addess-level1" regexp.
        //
        // Since "united state" string matches to the regexp of address-line2&3,
        // the two regexps should be excluded here.
        if (["address-level1", "address-line2", "address-line3"].includes(regexp)) {
          string = string.toLowerCase().split("united state").join("");
        }
        if (this.RULES[regexp].test(string)) {
          return regexp;
        }
      }
    }

    return null;
  },

  /**
   * Determine whether the regexp can match any of element strings.
   *
   * @param {HTMLElement} element
   * @param {RegExp} regexp
   *
   * @returns {boolean}
   */
  _matchRegexp(element, regexp) {
    const elemStrings = this._getElementStrings(element);
    for (const str of elemStrings) {
      if (regexp.test(str)) {
        return true;
      }
    }
    return false;
  },

/**
 * Phone field grammars - first matched grammar will be parsed. Grammars are
 * separated by { REGEX_SEPARATOR, FIELD_NONE, 0 }. Suffix and extension are
 * parsed separately unless they are necessary parts of the match.
 * The following notation is used to describe the patterns:
 * <cc> - country code field.
 * <ac> - area code field.
 * <phone> - phone or prefix.
 * <suffix> - suffix.
 * <ext> - extension.
 * :N means field is limited to N characters, otherwise it is unlimited.
 * (pattern <field>)? means pattern is optional and matched separately.
 *
 * This grammar list from Chromium will be enabled partially once we need to
 * support more cases of Telephone fields.
 */
  PHONE_FIELD_GRAMMARS: [
    // Country code: <cc> Area Code: <ac> Phone: <phone> (- <suffix>

    // (Ext: <ext>)?)?
      // {REGEX_COUNTRY, FIELD_COUNTRY_CODE, 0},
      // {REGEX_AREA, FIELD_AREA_CODE, 0},
      // {REGEX_PHONE, FIELD_PHONE, 0},
      // {REGEX_SEPARATOR, FIELD_NONE, 0},

    // \( <ac> \) <phone>:3 <suffix>:4 (Ext: <ext>)?
      // {REGEX_AREA_NOTEXT, FIELD_AREA_CODE, 3},
      // {REGEX_PREFIX_SEPARATOR, FIELD_PHONE, 3},
      // {REGEX_PHONE, FIELD_SUFFIX, 4},
      // {REGEX_SEPARATOR, FIELD_NONE, 0},

    // Phone: <cc> <ac>:3 - <phone>:3 - <suffix>:4 (Ext: <ext>)?
      // {REGEX_PHONE, FIELD_COUNTRY_CODE, 0},
      // {REGEX_PHONE, FIELD_AREA_CODE, 3},
      // {REGEX_PREFIX_SEPARATOR, FIELD_PHONE, 3},
      // {REGEX_SUFFIX_SEPARATOR, FIELD_SUFFIX, 4},
      // {REGEX_SEPARATOR, FIELD_NONE, 0},

    // Phone: <cc>:3 <ac>:3 <phone>:3 <suffix>:4 (Ext: <ext>)?
    ["tel", "tel-country-code", 3],
    ["tel", "tel-area-code", 3],
    ["tel", "tel-local-prefix", 3],
    ["tel", "tel-local-suffix", 4],
    [null, null, 0],

    // Area Code: <ac> Phone: <phone> (- <suffix> (Ext: <ext>)?)?
      // {REGEX_AREA, FIELD_AREA_CODE, 0},
      // {REGEX_PHONE, FIELD_PHONE, 0},
      // {REGEX_SEPARATOR, FIELD_NONE, 0},

    // Phone: <ac> <phone>:3 <suffix>:4 (Ext: <ext>)?
      // {REGEX_PHONE, FIELD_AREA_CODE, 0},
      // {REGEX_PHONE, FIELD_PHONE, 3},
      // {REGEX_PHONE, FIELD_SUFFIX, 4},
      // {REGEX_SEPARATOR, FIELD_NONE, 0},

    // Phone: <cc> \( <ac> \) <phone> (- <suffix> (Ext: <ext>)?)?
      // {REGEX_PHONE, FIELD_COUNTRY_CODE, 0},
      // {REGEX_AREA_NOTEXT, FIELD_AREA_CODE, 0},
      // {REGEX_PREFIX_SEPARATOR, FIELD_PHONE, 0},
      // {REGEX_SEPARATOR, FIELD_NONE, 0},

    // Phone: \( <ac> \) <phone> (- <suffix> (Ext: <ext>)?)?
      // {REGEX_PHONE, FIELD_COUNTRY_CODE, 0},
      // {REGEX_AREA_NOTEXT, FIELD_AREA_CODE, 0},
      // {REGEX_PREFIX_SEPARATOR, FIELD_PHONE, 0},
      // {REGEX_SEPARATOR, FIELD_NONE, 0},

    // Phone: <cc> - <ac> - <phone> - <suffix> (Ext: <ext>)?
      // {REGEX_PHONE, FIELD_COUNTRY_CODE, 0},
      // {REGEX_PREFIX_SEPARATOR, FIELD_AREA_CODE, 0},
      // {REGEX_PREFIX_SEPARATOR, FIELD_PHONE, 0},
      // {REGEX_SUFFIX_SEPARATOR, FIELD_SUFFIX, 0},
      // {REGEX_SEPARATOR, FIELD_NONE, 0},

    // Area code: <ac>:3 Prefix: <prefix>:3 Suffix: <suffix>:4 (Ext: <ext>)?
      // {REGEX_AREA, FIELD_AREA_CODE, 3},
      // {REGEX_PREFIX, FIELD_PHONE, 3},
      // {REGEX_SUFFIX, FIELD_SUFFIX, 4},
      // {REGEX_SEPARATOR, FIELD_NONE, 0},

    // Phone: <ac> Prefix: <phone> Suffix: <suffix> (Ext: <ext>)?
      // {REGEX_PHONE, FIELD_AREA_CODE, 0},
      // {REGEX_PREFIX, FIELD_PHONE, 0},
      // {REGEX_SUFFIX, FIELD_SUFFIX, 0},
      // {REGEX_SEPARATOR, FIELD_NONE, 0},

    // Phone: <ac> - <phone>:3 - <suffix>:4 (Ext: <ext>)?
    ["tel", "tel-area-code", 0],
    ["tel", "tel-local-prefix", 3],
    ["tel", "tel-local-suffix", 4],
    [null, null, 0],

    // Phone: <cc> - <ac> - <phone> (Ext: <ext>)?
      // {REGEX_PHONE, FIELD_COUNTRY_CODE, 0},
      // {REGEX_PREFIX_SEPARATOR, FIELD_AREA_CODE, 0},
      // {REGEX_SUFFIX_SEPARATOR, FIELD_PHONE, 0},
      // {REGEX_SEPARATOR, FIELD_NONE, 0},

    // Phone: <ac> - <phone> (Ext: <ext>)?
      // {REGEX_AREA, FIELD_AREA_CODE, 0},
      // {REGEX_PHONE, FIELD_PHONE, 0},
      // {REGEX_SEPARATOR, FIELD_NONE, 0},

    // Phone: <cc>:3 - <phone>:10 (Ext: <ext>)?
      // {REGEX_PHONE, FIELD_COUNTRY_CODE, 3},
      // {REGEX_PHONE, FIELD_PHONE, 10},
      // {REGEX_SEPARATOR, FIELD_NONE, 0},

    // Ext: <ext>
      // {REGEX_EXTENSION, FIELD_EXTENSION, 0},
      // {REGEX_SEPARATOR, FIELD_NONE, 0},

    // Phone: <phone> (Ext: <ext>)?
      // {REGEX_PHONE, FIELD_PHONE, 0},
      // {REGEX_SEPARATOR, FIELD_NONE, 0},
  ],
};

XPCOMUtils.defineLazyGetter(this.FormAutofillHeuristics, "RULES", () => {
  let sandbox = {};
  const HEURISTICS_REGEXP = "chrome://formautofill/content/heuristicsRegexp.js";
  Services.scriptloader.loadSubScript(HEURISTICS_REGEXP, sandbox, "utf-8");
  return sandbox.HeuristicsRegExp.RULES;
});

XPCOMUtils.defineLazyGetter(this.FormAutofillHeuristics, "_prefEnabled", () => {
  return Services.prefs.getBoolPref(PREF_HEURISTICS_ENABLED);
});

Services.prefs.addObserver(PREF_HEURISTICS_ENABLED, () => {
  this.FormAutofillHeuristics._prefEnabled = Services.prefs.getBoolPref(PREF_HEURISTICS_ENABLED);
});

XPCOMUtils.defineLazyGetter(this.FormAutofillHeuristics, "_sectionEnabled", () => {
  return Services.prefs.getBoolPref(PREF_SECTION_ENABLED);
});

Services.prefs.addObserver(PREF_SECTION_ENABLED, () => {
  this.FormAutofillHeuristics._sectionEnabled = Services.prefs.getBoolPref(PREF_SECTION_ENABLED);
});
PK
!<Ûgl¶$¶$$chrome/res/FormAutofillNameUtils.jsm/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

// 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";

var EXPORTED_SYMBOLS = ["FormAutofillNameUtils"];

ChromeUtils.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: [],
  KOREAN_MULTI_CHAR_SURNAMES: [],

  // The whitespace definition based on
  // https://cs.chromium.org/chromium/src/base/strings/string_util_constants.cc?l=9&rcl=b861deff77abecff11ae6a9f6946e9cc844b9817
  WHITESPACE: [
    "\u0009", // CHARACTER TABULATION
    "\u000A", // LINE FEED (LF)
    "\u000B", // LINE TABULATION
    "\u000C", // FORM FEED (FF)
    "\u000D", // CARRIAGE RETURN (CR)
    "\u0020", // SPACE
    "\u0085", // NEXT LINE (NEL)
    "\u00A0", // NO-BREAK SPACE
    "\u1680", // OGHAM SPACE MARK
    "\u2000", // EN QUAD
    "\u2001", // EM QUAD
    "\u2002", // EN SPACE
    "\u2003", // EM SPACE
    "\u2004", // THREE-PER-EM SPACE
    "\u2005", // FOUR-PER-EM SPACE
    "\u2006", // SIX-PER-EM SPACE
    "\u2007", // FIGURE SPACE
    "\u2008", // PUNCTUATION SPACE
    "\u2009", // THIN SPACE
    "\u200A", // HAIR SPACE
    "\u2028", // LINE SEPARATOR
    "\u2029", // PARAGRAPH SEPARATOR
    "\u202F", // NARROW NO-BREAK SPACE
    "\u205F", // MEDIUM MATHEMATICAL SPACE
    "\u3000", // IDEOGRAPHIC SPACE
  ],

  // The middle dot is used as a separator for foreign names in Japanese.
  MIDDLE_DOT: [
    "\u30FB", // KATAKANA MIDDLE DOT
    "\u00B7", // A (common?) typo for "KATAKANA MIDDLE DOT"
  ],

  // The Unicode range is based on Wiki:
  // https://en.wikipedia.org/wiki/CJK_Unified_Ideographs
  // https://en.wikipedia.org/wiki/Hangul
  // https://en.wikipedia.org/wiki/Japanese_writing_system
  CJK_RANGE: [
    "\u1100-\u11FF", // Hangul Jamo
    "\u3040-\u309F", // Hiragana
    "\u30A0-\u30FF", // Katakana
    "\u3105-\u312C", // Bopomofo
    "\u3130-\u318F", // Hangul Compatibility Jamo
    "\u31F0-\u31FF", // Katakana Phonetic Extensions
    "\u3200-\u32FF", // Enclosed CJK Letters and Months
    "\u3400-\u4DBF", // CJK unified ideographs Extension A
    "\u4E00-\u9FFF", // CJK Unified Ideographs
    "\uA960-\uA97F", // Hangul Jamo Extended-A
    "\uAC00-\uD7AF", // Hangul Syllables
    "\uD7B0-\uD7FF", // Hangul Jamo Extended-B
    "\uFF00-\uFFEF", // Halfwidth and Fullwidth Forms
  ],

  HANGUL_RANGE: [
    "\u1100-\u11FF", // Hangul Jamo
    "\u3130-\u318F", // Hangul Compatibility Jamo
    "\uA960-\uA97F", // Hangul Jamo Extended-A
    "\uAC00-\uD7AF", // Hangul Syllables
    "\uD7B0-\uD7FF", // Hangul Jamo Extended-B
  ],

  _dataLoaded: false,

  // Returns true if |set| contains |token|, modulo a final period.
  _containsString(set, token) {
    let target = token.replace(/\.$/, "").toLowerCase();
    return set.includes(target);
  },

  // Removes common name prefixes from |name_tokens|.
  _stripPrefixes(nameTokens) {
    for (let i in nameTokens) {
      if (!this._containsString(this.NAME_PREFIXES, nameTokens[i])) {
        return nameTokens.slice(i);
      }
    }
    return [];
  },

  // Removes common name suffixes from |name_tokens|.
  _stripSuffixes(nameTokens) {
    for (let i = nameTokens.length - 1; i >= 0; i--) {
      if (!this._containsString(this.NAME_SUFFIXES, nameTokens[i])) {
        return nameTokens.slice(0, i + 1);
      }
    }
    return [];
  },

  _isCJKName(name) {
    // The name is considered to be a CJK name if it is only CJK characters,
    // spaces, and "middle dot" separators, with at least one CJK character, and
    // no more than 2 words.
    //
    // Chinese and Japanese names are usually spelled out using the Han
    // characters (logographs), which constitute the "CJK Unified Ideographs"
    // block in Unicode, also referred to as Unihan. Korean names are usually
    // spelled out in the Korean alphabet (Hangul), although they do have a Han
    // equivalent as well.

    if (!name) {
      return false;
    }

    let previousWasCJK = false;
    let wordCount = 0;

    for (let c of name) {
      let isMiddleDot = this.MIDDLE_DOT.includes(c);
      let isCJK = !isMiddleDot && this.reCJK.test(c);
      if (!isCJK && !isMiddleDot && !this.WHITESPACE.includes(c)) {
        return false;
      }
      if (isCJK && !previousWasCJK) {
        wordCount++;
      }
      previousWasCJK = isCJK;
    }

    return wordCount > 0 && wordCount < 3;
  },

  // Tries to split a Chinese, Japanese, or Korean name into its given name &
  // surname parts. If splitting did not work for whatever reason, returns null.
  _splitCJKName(nameTokens) {
    // The convention for CJK languages is to put the surname (last name) first,
    // and the given name (first name) second. In a continuous text, there is
    // normally no space between the two parts of the name. When entering their
    // name into a field, though, some people add a space to disambiguate. CJK
    // names (almost) never have a middle name.

    let reHangulName = new RegExp(
      "^[" + this.HANGUL_RANGE.join("") + this.WHITESPACE.join("") + "]+$", "u");
    let nameParts = {
      given: "",
      middle: "",
      family: "",
    };

    if (nameTokens.length == 1) {
      // There is no space between the surname and given name. Try to infer
      // where to separate between the two. Most Chinese and Korean surnames
      // have only one character, but there are a few that have 2. If the name
      // does not start with a surname from a known list, default to one
      // character.
      let name = nameTokens[0];
      let isKorean = reHangulName.test(name);
      let surnameLength = 0;

      // 4-character Korean names are more likely to be 2/2 than 1/3, so use
      // the full list of Korean 2-char surnames. (instead of only the common
      // ones)
      let multiCharSurnames = (isKorean && name.length > 3) ?
        this.KOREAN_MULTI_CHAR_SURNAMES :
        this.COMMON_CJK_MULTI_CHAR_SURNAMES;

      // Default to 1 character if the surname is not in the list.
      surnameLength =
        multiCharSurnames.some(surname => name.startsWith(surname)) ? 2 : 1;

      nameParts.family = name.substr(0, surnameLength);
      nameParts.given = name.substr(surnameLength);
    } else if (nameTokens.length == 2) {
      // The user entered a space between the two name parts. This makes our job
      // easier. Family name first, given name second.
      nameParts.family = nameTokens[0];
      nameParts.given = nameTokens[1];
    } else {
      return null;
    }

    return nameParts;
  },

  init() {
    if (this._dataLoaded) {
      return;
    }
    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 nameParts = {
      given: "",
      middle: "",
      family: "",
    };

    if (!name) {
      return nameParts;
    }

    let nameTokens = name.trim().split(/[ ,\u3000\u30FB\u00B7]+/);
    nameTokens = this._stripPrefixes(nameTokens);

    if (this._isCJKName(name)) {
      let parts = this._splitCJKName(nameTokens);
      if (parts) {
        return parts;
      }
    }

    // Don't assume "Ma" is a suffix in John Ma.
    if (nameTokens.length > 2) {
      nameTokens = this._stripSuffixes(nameTokens);
    }

    if (!nameTokens.length) {
      // Bad things have happened; just assume the whole thing is a given name.
      nameParts.given = name;
      return nameParts;
    }

    // Only one token, assume given name.
    if (nameTokens.length == 1) {
      nameParts.given = nameTokens[0];
      return nameParts;
    }

    // 2 or more tokens. Grab the family, which is the last word plus any
    // recognizable family prefixes.
    let familyTokens = [nameTokens.pop()];
    while (nameTokens.length) {
      let lastToken = nameTokens[nameTokens.length - 1];
      if (!this._containsString(this.FAMILY_NAME_PREFIXES, lastToken)) {
        break;
      }
      familyTokens.unshift(lastToken);
      nameTokens.pop();
    }
    nameParts.family = familyTokens.join(" ");

    // Take the last remaining token as the middle name (if there are at least 2
    // tokens).
    if (nameTokens.length >= 2) {
      nameParts.middle = nameTokens.pop();
    }

    // Remainder is given name.
    nameParts.given = nameTokens.join(" ");

    return nameParts;
  },

  joinNameParts({given, middle, family}) {
    if (this._isCJKName(given) && this._isCJKName(family) && !middle) {
      return family + given;
    }
    return [given, middle, family].filter(part => part && part.length).join(" ");
  },
};

FormAutofillNameUtils.init();
PK
!<n<€ҩZ©Z!chrome/res/FormAutofillParent.jsm/* 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/. */

/*
 * Implements a service used to access storage and communicate with content.
 *
 * A "fields" array is used to communicate with FormAutofillContent. Each item
 * represents a single input field in the content page as well as its
 * @autocomplete properties. The schema is as below. Please refer to
 * FormAutofillContent.js for more details.
 *
 * [
 *   {
 *     section,
 *     addressType,
 *     contactType,
 *     fieldName,
 *     value,
 *     index
 *   },
 *   {
 *     // ...
 *   }
 * ]
 */

"use strict";

// We expose a singleton from this module. Some tests may import the
// constructor via a backstage pass.
var EXPORTED_SYMBOLS = ["formAutofillParent"];

ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");

ChromeUtils.import("resource://formautofill/FormAutofill.jsm");

XPCOMUtils.defineLazyModuleGetters(this, {
  BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
  CreditCard: "resource://gre/modules/CreditCard.jsm",
  FormAutofillPreferences: "resource://formautofill/FormAutofillPreferences.jsm",
  FormAutofillDoorhanger: "resource://formautofill/FormAutofillDoorhanger.jsm",
  FormAutofillUtils: "resource://formautofill/FormAutofillUtils.jsm",
  OSKeyStore: "resource://formautofill/OSKeyStore.jsm",
});

this.log = null;
FormAutofill.defineLazyLogGetter(this, EXPORTED_SYMBOLS[0]);

const {
  ENABLED_AUTOFILL_ADDRESSES_PREF,
  ENABLED_AUTOFILL_CREDITCARDS_PREF,
} = FormAutofill;

const {
  ADDRESSES_COLLECTION_NAME,
  CREDITCARDS_COLLECTION_NAME,
} = FormAutofillUtils;

function FormAutofillParent() {
  // Lazily load the storage JSM to avoid disk I/O until absolutely needed.
  // Once storage is loaded we need to update saved field names and inform content processes.
  XPCOMUtils.defineLazyGetter(this, "formAutofillStorage", () => {
    let {formAutofillStorage} = ChromeUtils.import("resource://formautofill/FormAutofillStorage.jsm", {});
    log.debug("Loading formAutofillStorage");

    formAutofillStorage.initialize().then(() => {
      // Update the saved field names to compute the status and update child processes.
      this._updateSavedFieldNames();
    });

    return formAutofillStorage;
  });
}

FormAutofillParent.prototype = {
  QueryInterface: ChromeUtils.generateQI([Ci.nsIObserver]),

  /**
   * Cache of the Form Autofill status (considering preferences and storage).
   */
  _active: null,

  /**
   * The status of Form Autofill's initialization.
   */
  _initialized: false,

  /**
   * Exposes the status of Form Autofill's initialization. It can be used to
   * determine whether Form Autofill is available for current users.
   *
   * @returns {boolean} Whether FormAutofillParent is initialized.
   */
  get initialized() {
    return this._initialized;
  },

  /**
   * Initializes FormAutofillStorage and registers the message handler.
   */
  async init() {
    if (this._initialized) {
      return;
    }
    this._initialized = true;

    Services.obs.addObserver(this, "sync-pane-loaded");
    Services.ppmm.addMessageListener("FormAutofill:InitStorage", this);
    Services.ppmm.addMessageListener("FormAutofill:GetRecords", this);
    Services.ppmm.addMessageListener("FormAutofill:SaveAddress", this);
    Services.ppmm.addMessageListener("FormAutofill:RemoveAddresses", this);
    Services.ppmm.addMessageListener("FormAutofill:OpenPreferences", this);
    Services.mm.addMessageListener("FormAutofill:OnFormSubmit", this);

    // Observing the pref and storage changes
    Services.prefs.addObserver(ENABLED_AUTOFILL_ADDRESSES_PREF, this);
    Services.obs.addObserver(this, "formautofill-storage-changed");

    // Only listen to credit card related messages if it is available
    if (FormAutofill.isAutofillCreditCardsAvailable) {
      Services.ppmm.addMessageListener("FormAutofill:SaveCreditCard", this);
      Services.ppmm.addMessageListener("FormAutofill:RemoveCreditCards", this);
      Services.ppmm.addMessageListener("FormAutofill:GetDecryptedString", this);
      Services.prefs.addObserver(ENABLED_AUTOFILL_CREDITCARDS_PREF, this);
    }
  },

  observe(subject, topic, data) {
    log.debug("observe:", topic, "with data:", data);
    switch (topic) {
      case "sync-pane-loaded": {
        let formAutofillPreferences = new FormAutofillPreferences();
        let document = subject.document;
        let prefFragment = formAutofillPreferences.init(document);
        let formAutofillGroupBox = document.getElementById("formAutofillGroupBox");
        formAutofillGroupBox.appendChild(prefFragment);
        break;
      }

      case "nsPref:changed": {
        // Observe pref changes and update _active cache if status is changed.
        this._updateStatus();
        break;
      }

      case "formautofill-storage-changed": {
        // Early exit if only metadata is changed
        if (data == "notifyUsed") {
          break;
        }

        this._updateSavedFieldNames();
        break;
      }

      default: {
        throw new Error(`FormAutofillParent: Unexpected topic observed: ${topic}`);
      }
    }
  },

  /**
   * Broadcast the status to frames when the form autofill status changes.
   */
  _onStatusChanged() {
    log.debug("_onStatusChanged: Status changed to", this._active);
    Services.ppmm.broadcastAsyncMessage("FormAutofill:enabledStatus", this._active);
    // Sync process data autofillEnabled to make sure the value up to date
    // no matter when the new content process is initialized.
    Services.ppmm.initialProcessData.autofillEnabled = this._active;
  },

  /**
   * Query preference and storage status to determine the overall status of the
   * form autofill feature.
   *
   * @returns {boolean} whether form autofill is active (enabled and has data)
   */
  _computeStatus() {
    const savedFieldNames = Services.ppmm.initialProcessData.autofillSavedFieldNames;

    return (Services.prefs.getBoolPref(ENABLED_AUTOFILL_ADDRESSES_PREF) ||
           Services.prefs.getBoolPref(ENABLED_AUTOFILL_CREDITCARDS_PREF)) &&
           savedFieldNames &&
           savedFieldNames.size > 0;
  },

  /**
   * Update the status and trigger _onStatusChanged, if necessary.
   */
  _updateStatus() {
    log.debug("_updateStatus");
    let wasActive = this._active;
    this._active = this._computeStatus();
    if (this._active !== wasActive) {
      this._onStatusChanged();
    }
  },

  /**
   * Handles the message coming from FormAutofillContent.
   *
   * @param   {string} message.name The name of the message.
   * @param   {object} message.data The data of the message.
   * @param   {nsIFrameMessageManager} message.target Caller's message manager.
   */
  async receiveMessage({name, data, target}) {
    switch (name) {
      case "FormAutofill:InitStorage": {
        await this.formAutofillStorage.initialize();
        break;
      }
      case "FormAutofill:GetRecords": {
        await this._getRecords(data, target);
        break;
      }
      case "FormAutofill:SaveAddress": {
        if (data.guid) {
          await this.formAutofillStorage.addresses.update(data.guid, data.address);
        } else {
          await this.formAutofillStorage.addresses.add(data.address);
        }
        break;
      }
      case "FormAutofill:SaveCreditCard": {
        if (!await OSKeyStore.ensureLoggedIn()) {
          log.warn("User canceled encryption login");
          return;
        }
        await this.formAutofillStorage.creditCards.add(data.creditcard);
        break;
      }
      case "FormAutofill:RemoveAddresses": {
        data.guids.forEach(guid => this.formAutofillStorage.addresses.remove(guid));
        break;
      }
      case "FormAutofill:RemoveCreditCards": {
        data.guids.forEach(guid => this.formAutofillStorage.creditCards.remove(guid));
        break;
      }
      case "FormAutofill:OnFormSubmit": {
        await this._onFormSubmit(data, target);
        break;
      }
      case "FormAutofill:OpenPreferences": {
        const win = BrowserWindowTracker.getTopWindow();
        win.openPreferences("privacy-form-autofill", {origin: "autofillFooter"});
        break;
      }
      case "FormAutofill:GetDecryptedString": {
        let {cipherText, reauth} = data;
        let string;
        try {
          string = await OSKeyStore.decrypt(cipherText, reauth);
        } catch (e) {
          if (e.result != Cr.NS_ERROR_ABORT) {
            throw e;
          }
          log.warn("User canceled encryption login");
        }
        target.sendAsyncMessage("FormAutofill:DecryptedString", string);
        break;
      }
    }
  },

  /**
   * Uninitializes FormAutofillParent. This is for testing only.
   *
   * @private
   */
  _uninit() {
    this.formAutofillStorage._saveImmediately();

    Services.ppmm.removeMessageListener("FormAutofill:InitStorage", this);
    Services.ppmm.removeMessageListener("FormAutofill:GetRecords", this);
    Services.ppmm.removeMessageListener("FormAutofill:SaveAddress", this);
    Services.ppmm.removeMessageListener("FormAutofill:RemoveAddresses", this);
    Services.obs.removeObserver(this, "sync-pane-loaded");
    Services.prefs.removeObserver(ENABLED_AUTOFILL_ADDRESSES_PREF, this);

    if (FormAutofill.isAutofillCreditCardsAvailable) {
      Services.ppmm.removeMessageListener("FormAutofill:SaveCreditCard", this);
      Services.ppmm.removeMessageListener("FormAutofill:RemoveCreditCards", this);
      Services.ppmm.removeMessageListener("FormAutofill:GetDecryptedString", this);
      Services.prefs.removeObserver(ENABLED_AUTOFILL_CREDITCARDS_PREF, this);
    }
  },

  /**
   * Get the records from profile store and return results back to content
   * process. It will decrypt the credit card number and append
   * "cc-number-decrypted" to each record if OSKeyStore isn't set.
   *
   * @private
   * @param  {string} data.collectionName
   *         The name used to specify which collection to retrieve records.
   * @param  {string} data.searchString
   *         The typed string for filtering out the matched records.
   * @param  {string} data.info
   *         The input autocomplete property's information.
   * @param  {nsIFrameMessageManager} target
   *         Content's message manager.
   */
  async _getRecords({collectionName, searchString, info}, target) {
    let collection = this.formAutofillStorage[collectionName];
    if (!collection) {
      target.sendAsyncMessage("FormAutofill:Records", []);
      return;
    }

    let recordsInCollection = await collection.getAll();
    if (!info || !info.fieldName || !recordsInCollection.length) {
      target.sendAsyncMessage("FormAutofill:Records", recordsInCollection);
      return;
    }

    let isCC = collectionName == CREDITCARDS_COLLECTION_NAME;
    // We don't filter "cc-number"
    if (isCC && info.fieldName == "cc-number") {
      recordsInCollection = recordsInCollection.filter(record => !!record["cc-number"]);
      target.sendAsyncMessage("FormAutofill:Records", recordsInCollection);
      return;
    }

    let records = [];
    let lcSearchString = searchString.toLowerCase();

    for (let record of recordsInCollection) {
      let fieldValue = record[info.fieldName];
      if (!fieldValue) {
        continue;
      }

      if (collectionName == ADDRESSES_COLLECTION_NAME && record.country
          && !FormAutofill.supportedCountries.includes(record.country)) {
        // Address autofill isn't supported for the record's country so we don't
        // want to attempt to potentially incorrectly fill the address fields.
        continue;
      }

      if (lcSearchString && !String(fieldValue).toLowerCase().startsWith(lcSearchString)) {
        continue;
      }
      records.push(record);
    }

    target.sendAsyncMessage("FormAutofill:Records", records);
  },

  _updateSavedFieldNames() {
    log.debug("_updateSavedFieldNames");

    // Don't access the credit cards store unless it is enabled.
    if (FormAutofill.isAutofillCreditCardsAvailable) {
      Services.ppmm.initialProcessData.autofillSavedFieldNames =
        new Set([...this.formAutofillStorage.addresses.getSavedFieldNames(),
          ...this.formAutofillStorage.creditCards.getSavedFieldNames()]);
    } else {
      Services.ppmm.initialProcessData.autofillSavedFieldNames =
        this.formAutofillStorage.addresses.getSavedFieldNames();
    }

    Services.ppmm.broadcastAsyncMessage("FormAutofill:savedFieldNames",
                                        Services.ppmm.initialProcessData.autofillSavedFieldNames);
    this._updateStatus();
  },

  async _onAddressSubmit(address, target, timeStartedFillingMS) {
    let showDoorhanger = null;
    if (address.guid) {
      // Avoid updating the fields that users don't modify.
      let originalAddress = await this.formAutofillStorage.addresses.get(address.guid);
      for (let field in address.record) {
        if (address.untouchedFields.includes(field) && originalAddress[field]) {
          address.record[field] = originalAddress[field];
        }
      }

      if (!await this.formAutofillStorage.addresses.mergeIfPossible(address.guid, address.record, true)) {
        this._recordFormFillingTime("address", "autofill-update", timeStartedFillingMS);

        showDoorhanger = async () => {
          const description = FormAutofillUtils.getAddressLabel(address.record);
          const state = await FormAutofillDoorhanger.show(target, "updateAddress", description);
          let changedGUIDs = await this.formAutofillStorage.addresses.mergeToStorage(address.record, true);
          switch (state) {
            case "create":
              if (!changedGUIDs.length) {
                changedGUIDs.push(await this.formAutofillStorage.addresses.add(address.record));
              }
              break;
            case "update":
              if (!changedGUIDs.length) {
                await this.formAutofillStorage.addresses.update(address.guid, address.record, true);
                changedGUIDs.push(address.guid);
              } else {
                this.formAutofillStorage.addresses.remove(address.guid);
              }
              break;
          }
          changedGUIDs.forEach(guid => this.formAutofillStorage.addresses.notifyUsed(guid));
        };
        // Address should be updated
        Services.telemetry.scalarAdd("formautofill.addresses.fill_type_autofill_update", 1);
      } else {
        this._recordFormFillingTime("address", "autofill", timeStartedFillingMS);
        this.formAutofillStorage.addresses.notifyUsed(address.guid);
        // Address is merged successfully
        Services.telemetry.scalarAdd("formautofill.addresses.fill_type_autofill", 1);
      }
    } else {
      let changedGUIDs = await this.formAutofillStorage.addresses.mergeToStorage(address.record);
      if (!changedGUIDs.length) {
        changedGUIDs.push(await this.formAutofillStorage.addresses.add(address.record));
      }
      changedGUIDs.forEach(guid => this.formAutofillStorage.addresses.notifyUsed(guid));
      this._recordFormFillingTime("address", "manual", timeStartedFillingMS);

      // Show first time use doorhanger
      if (FormAutofill.isAutofillAddressesFirstTimeUse) {
        Services.prefs.setBoolPref(FormAutofill.ADDRESSES_FIRST_TIME_USE_PREF, false);
        showDoorhanger = async () => {
          const description = FormAutofillUtils.getAddressLabel(address.record);
          const state = await FormAutofillDoorhanger.show(target, "firstTimeUse", description);
          if (state !== "open-pref") {
            return;
          }

          target.ownerGlobal.openPreferences("privacy-address-autofill",
                                             {origin: "autofillDoorhanger"});
        };
      } else {
        // We want to exclude the first time form filling.
        Services.telemetry.scalarAdd("formautofill.addresses.fill_type_manual", 1);
      }
    }
    return showDoorhanger;
  },

  async _onCreditCardSubmit(creditCard, target, timeStartedFillingMS) {
    // Updates the used status for shield/heartbeat to recognize users who have
    // used Credit Card Autofill.
    let setUsedStatus = status => {
      if (FormAutofill.AutofillCreditCardsUsedStatus < status) {
        Services.prefs.setIntPref(FormAutofill.CREDITCARDS_USED_STATUS_PREF, status);
      }
    };

    // Remove invalid cc-type values
    if (creditCard.record["cc-type"] && !CreditCard.isValidNetwork(creditCard.record["cc-type"])) {
      delete creditCard.record["cc-type"];
    }

    // We'll show the credit card doorhanger if:
    //   - User applys autofill and changed
    //   - User fills form manually and the filling data is not duplicated to storage
    if (creditCard.guid) {
      // Indicate that the user has used Credit Card Autofill to fill in a form.
      setUsedStatus(3);

      let originalCCData = await this.formAutofillStorage.creditCards.get(creditCard.guid);
      let recordUnchanged = true;
      for (let field in creditCard.record) {
        if (creditCard.record[field] === "" && !originalCCData[field]) {
          continue;
        }
        // Avoid updating the fields that users don't modify, but skip number field
        // because we don't want to trigger decryption here.
        let untouched = creditCard.untouchedFields.includes(field);
        if (untouched && field !== "cc-number") {
          creditCard.record[field] = originalCCData[field];
        }
        // recordUnchanged will be false if one of the field is changed.
        recordUnchanged &= untouched;
      }

      if (recordUnchanged) {
        this.formAutofillStorage.creditCards.notifyUsed(creditCard.guid);
        // Add probe to record credit card autofill(without modification).
        Services.telemetry.scalarAdd("formautofill.creditCards.fill_type_autofill", 1);
        this._recordFormFillingTime("creditCard", "autofill", timeStartedFillingMS);
        return false;
      }
      // Add the probe to record credit card autofill with modification.
      Services.telemetry.scalarAdd("formautofill.creditCards.fill_type_autofill_modified", 1);
      this._recordFormFillingTime("creditCard", "autofill-update", timeStartedFillingMS);
    } else {
      // Indicate that the user neither sees the doorhanger nor uses Autofill
      // but somehow has a duplicate record in the storage. Will be reset to 2
      // if the doorhanger actually shows below.
      setUsedStatus(1);

      // Add the probe to record credit card manual filling.
      Services.telemetry.scalarAdd("formautofill.creditCards.fill_type_manual", 1);
      this._recordFormFillingTime("creditCard", "manual", timeStartedFillingMS);
    }

    // Early return if it's a duplicate data
    let dupGuid = await this.formAutofillStorage.creditCards.getDuplicateGuid(creditCard.record);
    if (dupGuid) {
      this.formAutofillStorage.creditCards.notifyUsed(dupGuid);
      return false;
    }

    // Indicate that the user has seen the doorhanger.
    setUsedStatus(2);

    return async () => {
      // Suppress the pending doorhanger from showing up if user disabled credit card in previous doorhanger.
      if (!FormAutofill.isAutofillCreditCardsEnabled) {
        return;
      }

      let number = creditCard.record["cc-number"] || creditCard.record["cc-number-decrypted"];
      let name = creditCard.record["cc-name"];
      const description = await CreditCard.getLabel({name, number});
      const state = await FormAutofillDoorhanger.show(target,
                                                      creditCard.guid ? "updateCreditCard" : "addCreditCard",
                                                      description);
      if (state == "cancel") {
        return;
      }

      if (state == "disable") {
        Services.prefs.setBoolPref("extensions.formautofill.creditCards.enabled", false);
        return;
      }

      if (!await OSKeyStore.ensureLoggedIn()) {
        log.warn("User canceled encryption login");
        return;
      }

      let changedGUIDs = [];
      if (creditCard.guid) {
        if (state == "update") {
          await this.formAutofillStorage.creditCards.update(creditCard.guid, creditCard.record, true);
          changedGUIDs.push(creditCard.guid);
        } else if ("create") {
          changedGUIDs.push(await this.formAutofillStorage.creditCards.add(creditCard.record));
        }
      } else {
        changedGUIDs.push(...await this.formAutofillStorage.creditCards.mergeToStorage(creditCard.record));
        if (!changedGUIDs.length) {
          changedGUIDs.push(await this.formAutofillStorage.creditCards.add(creditCard.record));
        }
      }
      changedGUIDs.forEach(guid => this.formAutofillStorage.creditCards.notifyUsed(guid));
    };
  },

  async _onFormSubmit(data, target) {
    let {profile: {address, creditCard}, timeStartedFillingMS} = data;

    // Don't record filling time if any type of records has more than one section being
    // populated. We've been recording the filling time, so the other cases that aren't
    // recorded on the same basis should be out of the data samples. E.g. Filling time of
    // populating one profile is different from populating two sections, therefore, we
    // shouldn't record the later to regress the representation of existing statistics.
    if (address.length > 1 || creditCard.length > 1) {
      timeStartedFillingMS = null;
    }

    // Transmit the telemetry immediately in the meantime form submitted, and handle these pending
    // doorhangers at a later.
    await Promise.all([
      await Promise.all(address.map(addrRecord => this._onAddressSubmit(addrRecord, target, timeStartedFillingMS))),
      await Promise.all(creditCard.map(ccRecord => this._onCreditCardSubmit(ccRecord, target, timeStartedFillingMS))),
    ].map(pendingDoorhangers => {
      return pendingDoorhangers.filter(pendingDoorhanger => !!pendingDoorhanger &&
                                                            typeof pendingDoorhanger == "function");
    }).map(pendingDoorhangers => new Promise(async resolve => {
      for (const showDoorhanger of pendingDoorhangers) {
        await showDoorhanger();
      }
      resolve();
    })));
  },
  /**
   * Set the probes for the filling time with specific filling type and form type.
   *
   * @private
   * @param  {string} formType
   *         3 type of form (address/creditcard/address-creditcard).
   * @param  {string} fillingType
   *         3 filling type (manual/autofill/autofill-update).
   * @param  {int|null} startedFillingMS
   *         Time that form started to filling in ms. Early return if start time is null.
   */
  _recordFormFillingTime(formType, fillingType, startedFillingMS) {
    if (!startedFillingMS) {
      return;
    }
    let histogram = Services.telemetry.getKeyedHistogramById("FORM_FILLING_REQUIRED_TIME_MS");
    histogram.add(`${formType}-${fillingType}`, Date.now() - startedFillingMS);
  },
};

var formAutofillParent = new FormAutofillParent();
PK
!<ü½<JX(X(&chrome/res/FormAutofillPreferences.jsm/* 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/. */

/**
 * Injects the form autofill section into about:preferences.
 */

"use strict";

var EXPORTED_SYMBOLS = ["FormAutofillPreferences"];

// Add addresses enabled flag in telemetry environment for recording the number of
// users who disable/enable the address autofill feature.
const BUNDLE_URI = "chrome://formautofill/locale/formautofill.properties";
const MANAGE_ADDRESSES_URL = "chrome://formautofill/content/manageAddresses.xhtml";
const MANAGE_CREDITCARDS_URL = "chrome://formautofill/content/manageCreditCards.xhtml";

ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://formautofill/FormAutofill.jsm");
ChromeUtils.import("resource://formautofill/FormAutofillUtils.jsm");

const {
  ENABLED_AUTOFILL_ADDRESSES_PREF,
  ENABLED_AUTOFILL_CREDITCARDS_PREF,
} = FormAutofill;
const {
  MANAGE_ADDRESSES_KEYWORDS,
  EDIT_ADDRESS_KEYWORDS,
  MANAGE_CREDITCARDS_KEYWORDS,
  EDIT_CREDITCARD_KEYWORDS,
} = FormAutofillUtils;
// Add credit card enabled flag in telemetry environment for recording the number of
// users who disable/enable the credit card autofill feature.

this.log = null;
FormAutofill.defineLazyLogGetter(this, EXPORTED_SYMBOLS[0]);

const HTML_NS = "http://www.w3.org/1999/xhtml";

function FormAutofillPreferences() {
  this.bundle = Services.strings.createBundle(BUNDLE_URI);
}

FormAutofillPreferences.prototype = {
  /**
   * Create the Form Autofill preference group.
   *
   * @param   {XULDocument} document
   * @returns {XULElement}
   */
  init(document) {
    this.createPreferenceGroup(document);
    this.attachEventListeners();

    return this.refs.formAutofillFragment;
  },

  /**
   * Remove event listeners and the preference group.
   */
  uninit() {
    this.detachEventListeners();
    this.refs.formAutofillGroup.remove();
  },

  /**
   * Create Form Autofill preference group
   *
   * @param  {XULDocument} document
   */
  createPreferenceGroup(document) {
    let learnMoreURL = Services.urlFormatter.formatURLPref("app.support.baseURL") + "autofill-card-address";
    let formAutofillFragment = document.createDocumentFragment();
    let formAutofillGroupBoxLabel = document.createXULElement("label");
    let formAutofillGroupBoxLabelHeading = document.createElementNS(HTML_NS, "h2");
    let formAutofillGroupBoxDescription = document.createXULElement("description");
    let formAutofillGroup = document.createXULElement("vbox");
    let addressAutofill = document.createXULElement("hbox");
    let addressAutofillCheckboxGroup = document.createXULElement("hbox");
    let addressAutofillCheckbox = document.createXULElement("checkbox");
    let addressAutofillLearnMore = document.createXULElement("label");
    let savedAddressesBtn = document.createXULElement("button");
    // Wrappers are used to properly compute the search tooltip positions
    let savedAddressesBtnWrapper = document.createXULElement("hbox");
    let savedCreditCardsBtnWrapper = document.createXULElement("hbox");

    savedAddressesBtn.className = "accessory-button";
    addressAutofillCheckbox.className = "tail-with-learn-more";
    addressAutofillLearnMore.className = "learnMore text-link";

    formAutofillGroup.id = "formAutofillGroup";
    addressAutofill.id = "addressAutofill";
    addressAutofillLearnMore.id = "addressAutofillLearnMore";

    formAutofillGroupBoxLabelHeading.textContent = this.bundle.GetStringFromName("autofillHeader");
    formAutofillGroupBoxDescription.textContent =
      this.bundle.formatStringFromName("autofillDescription",
                                       [FormAutofillUtils.brandBundle.GetStringFromName("brandShortName")],
                                       1);

    addressAutofill.setAttribute("data-subcategory", "address-autofill");
    addressAutofillCheckbox.setAttribute("label", this.bundle.GetStringFromName("autofillAddressesCheckbox"));
    addressAutofillLearnMore.textContent = this.bundle.GetStringFromName("learnMoreLabel");
    savedAddressesBtn.setAttribute("label", this.bundle.GetStringFromName("savedAddressesBtnLabel"));
    // Align the start to keep the savedAddressesBtn as original size
    // when addressAutofillCheckboxGroup's height is changed by a longer l10n string
    savedAddressesBtnWrapper.setAttribute("align", "start");

    addressAutofillLearnMore.setAttribute("href", learnMoreURL);

    // Add preferences search support
    savedAddressesBtn.setAttribute("searchkeywords", MANAGE_ADDRESSES_KEYWORDS.concat(EDIT_ADDRESS_KEYWORDS)
                                                       .map(key => this.bundle.GetStringFromName(key)).join("\n"));

    // Manually set the checked state
    if (FormAutofill.isAutofillAddressesEnabled) {
      addressAutofillCheckbox.setAttribute("checked", true);
    }

    addressAutofillCheckboxGroup.align = "center";
    addressAutofillCheckboxGroup.flex = 1;

    formAutofillGroupBoxLabel.appendChild(formAutofillGroupBoxLabelHeading);
    formAutofillFragment.appendChild(formAutofillGroupBoxLabel);
    formAutofillFragment.appendChild(formAutofillGroupBoxDescription);
    formAutofillFragment.appendChild(formAutofillGroup);
    formAutofillGroup.appendChild(addressAutofill);
    addressAutofill.appendChild(addressAutofillCheckboxGroup);
    addressAutofillCheckboxGroup.appendChild(addressAutofillCheckbox);
    addressAutofillCheckboxGroup.appendChild(addressAutofillLearnMore);
    addressAutofill.appendChild(savedAddressesBtnWrapper);
    savedAddressesBtnWrapper.appendChild(savedAddressesBtn);

    this.refs = {
      formAutofillFragment,
      formAutofillGroup,
      addressAutofillCheckbox,
      savedAddressesBtn,
    };

    if (FormAutofill.isAutofillCreditCardsAvailable) {
      let creditCardAutofill = document.createXULElement("hbox");
      let creditCardAutofillCheckboxGroup = document.createXULElement("hbox");
      let creditCardAutofillCheckbox = document.createXULElement("checkbox");
      let creditCardAutofillLearnMore = document.createXULElement("label");
      let savedCreditCardsBtn = document.createXULElement("button");
      savedCreditCardsBtn.className = "accessory-button";
      creditCardAutofillCheckbox.className = "tail-with-learn-more";
      creditCardAutofillLearnMore.className = "learnMore text-link";

      creditCardAutofill.id = "creditCardAutofill";
      creditCardAutofillLearnMore.id = "creditCardAutofillLearnMore";

      creditCardAutofill.setAttribute("data-subcategory", "credit-card-autofill");
      creditCardAutofillCheckbox.setAttribute("label", this.bundle.GetStringFromName("autofillCreditCardsCheckbox"));
      creditCardAutofillLearnMore.textContent = this.bundle.GetStringFromName("learnMoreLabel");
      savedCreditCardsBtn.setAttribute("label", this.bundle.GetStringFromName("savedCreditCardsBtnLabel"));
      // Align the start to keep the savedCreditCardsBtn as original size
      // when creditCardAutofillCheckboxGroup's height is changed by a longer l10n string
      savedCreditCardsBtnWrapper.setAttribute("align", "start");

      creditCardAutofillLearnMore.setAttribute("href", learnMoreURL);

      // Add preferences search support
      savedCreditCardsBtn.setAttribute("searchkeywords", MANAGE_CREDITCARDS_KEYWORDS.concat(EDIT_CREDITCARD_KEYWORDS)
                                                           .map(key => this.bundle.GetStringFromName(key)).join("\n"));

      // Manually set the checked state
      if (FormAutofill.isAutofillCreditCardsEnabled) {
        creditCardAutofillCheckbox.setAttribute("checked", true);
      }

      creditCardAutofillCheckboxGroup.align = "center";
      creditCardAutofillCheckboxGroup.flex = 1;

      formAutofillGroup.appendChild(creditCardAutofill);
      creditCardAutofill.appendChild(creditCardAutofillCheckboxGroup);
      creditCardAutofillCheckboxGroup.appendChild(creditCardAutofillCheckbox);
      creditCardAutofillCheckboxGroup.appendChild(creditCardAutofillLearnMore);
      creditCardAutofill.appendChild(savedCreditCardsBtnWrapper);
      savedCreditCardsBtnWrapper.appendChild(savedCreditCardsBtn);

      this.refs.creditCardAutofillCheckbox = creditCardAutofillCheckbox;
      this.refs.savedCreditCardsBtn = savedCreditCardsBtn;
    }
  },

  /**
   * Handle events
   *
   * @param  {DOMEvent} event
   */
  handleEvent(event) {
    switch (event.type) {
      case "command": {
        let target = event.target;

        if (target == this.refs.addressAutofillCheckbox) {
          // Set preference directly instead of relying on <Preference>
          Services.prefs.setBoolPref(ENABLED_AUTOFILL_ADDRESSES_PREF, target.checked);
        } else if (target == this.refs.creditCardAutofillCheckbox) {
          Services.prefs.setBoolPref(ENABLED_AUTOFILL_CREDITCARDS_PREF, target.checked);
        } else if (target == this.refs.savedAddressesBtn) {
          target.ownerGlobal.gSubDialog.open(MANAGE_ADDRESSES_URL);
        } else if (target == this.refs.savedCreditCardsBtn) {
          target.ownerGlobal.gSubDialog.open(MANAGE_CREDITCARDS_URL);
        }
        break;
      }
      case "click": {
        let target = event.target;

        if (target == this.refs.addressAutofillCheckboxLabel) {
          let pref = FormAutofill.isAutofillAddressesEnabled;
          Services.prefs.setBoolPref(ENABLED_AUTOFILL_ADDRESSES_PREF, !pref);
          this.refs.addressAutofillCheckbox.checked = !pref;
        } else if (target == this.refs.creditCardAutofillCheckboxLabel) {
          let pref = FormAutofill.isAutofillCreditCardsEnabled;
          Services.prefs.setBoolPref(ENABLED_AUTOFILL_CREDITCARDS_PREF, !pref);
          this.refs.creditCardAutofillCheckbox.checked = !pref;
        }
        break;
      }
    }
  },

  /**
   * Attach event listener
   */
  attachEventListeners() {
    this.refs.formAutofillGroup.addEventListener("command", this);
    this.refs.formAutofillGroup.addEventListener("click", this);
  },

  /**
   * Remove event listener
   */
  detachEventListeners() {
    this.refs.formAutofillGroup.removeEventListener("command", this);
    this.refs.formAutofillGroup.removeEventListener("click", this);
  },
};
PK
!<)Ï;TÊÊ"chrome/res/FormAutofillStorage.jsm/* 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/. */

/*
 * Implements an interface of the storage of Form Autofill.
 *
 * The data is stored in JSON format, without indentation and the computed
 * fields, using UTF-8 encoding. With indentation and computed fields applied,
 * the schema would look like this:
 *
 * {
 *   version: 1,
 *   addresses: [
 *     {
 *       guid,                 // 12 characters
 *       version,              // schema version in integer
 *
 *       // address fields
 *       given-name,
 *       additional-name,
 *       family-name,
 *       organization,         // Company
 *       street-address,       // (Multiline)
 *       address-level3,       // Suburb/Sublocality
 *       address-level2,       // City/Town
 *       address-level1,       // Province (Standardized code if possible)
 *       postal-code,
 *       country,              // ISO 3166
 *       tel,                  // Stored in E.164 format
 *       email,
 *
 *       // computed fields (These fields are computed based on the above fields
 *       // and are not allowed to be modified directly.)
 *       name,
 *       address-line1,
 *       address-line2,
 *       address-line3,
 *       country-name,
 *       tel-country-code,
 *       tel-national,
 *       tel-area-code,
 *       tel-local,
 *       tel-local-prefix,
 *       tel-local-suffix,
 *
 *       // metadata
 *       timeCreated,          // in ms
 *       timeLastUsed,         // in ms
 *       timeLastModified,     // in ms
 *       timesUsed
 *       _sync: { ... optional sync metadata },
 *     }
 *   ],
 *   creditCards: [
 *     {
 *       guid,                 // 12 characters
 *       version,              // schema version in integer
 *
 *       // credit card fields
 *       billingAddressGUID,   // An optional GUID of an autofill address record
 *                                which may or may not exist locally.
 *
 *       cc-name,
 *       cc-number,            // will be stored in masked format (************1234)
 *                             // (see details below)
 *       cc-exp-month,
 *       cc-exp-year,          // 2-digit year will be converted to 4 digits
 *                             // upon saving
 *       cc-type,              // Optional card network id (instrument type)
 *
 *       // computed fields (These fields are computed based on the above fields
 *       // and are not allowed to be modified directly.)
 *       cc-given-name,
 *       cc-additional-name,
 *       cc-family-name,
 *       cc-number-encrypted,  // encrypted from the original unmasked "cc-number"
 *                             // (see details below)
 *       cc-exp,
 *
 *       // metadata
 *       timeCreated,          // in ms
 *       timeLastUsed,         // in ms
 *       timeLastModified,     // in ms
 *       timesUsed
 *       _sync: { ... optional sync metadata },
 *     }
 *   ]
 * }
 *
 *
 * Encrypt-related Credit Card Fields (cc-number & cc-number-encrypted):
 *
 * When saving or updating a credit-card record, the storage will encrypt the
 * value of "cc-number", store the encrypted number in "cc-number-encrypted"
 * field, and replace "cc-number" field with the masked number. These all happen
 * in "computeFields". We do reverse actions in "_stripComputedFields", which
 * decrypts "cc-number-encrypted", restores it to "cc-number", and deletes
 * "cc-number-encrypted". Therefore, calling "_stripComputedFields" followed by
 * "computeFields" can make sure the encrypt-related fields are up-to-date.
 *
 * In general, you have to decrypt the number by your own outside FormAutofillStorage
 * when necessary. However, you will get the decrypted records when querying
 * data with "rawData=true" to ensure they're ready to sync.
 *
 *
 * Sync Metadata:
 *
 * Records may also have a _sync field, which consists of:
 * {
 *   changeCounter,    // integer - the number of changes made since the last
 *                     // sync.
 *   lastSyncedFields, // object - hashes of the original values for fields
 *                     // changed since the last sync.
 * }
 *
 * Records with such a field have previously been synced. Records without such
 * a field are yet to be synced, so are treated specially in some cases (eg,
 * they don't need a tombstone, de-duping logic treats them as special etc).
 * Records without the field are always considered "dirty" from Sync's POV
 * (meaning they will be synced on the next sync), at which time they will gain
 * this new field.
 */

"use strict";

// We expose a singleton from this module. Some tests may import the
// constructor via a backstage pass.
this.EXPORTED_SYMBOLS = ["formAutofillStorage"];

ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://gre/modules/osfile.jsm");

ChromeUtils.import("resource://formautofill/FormAutofill.jsm");

ChromeUtils.defineModuleGetter(this, "CreditCard",
                               "resource://gre/modules/CreditCard.jsm");
ChromeUtils.defineModuleGetter(this, "JSONFile",
                               "resource://gre/modules/JSONFile.jsm");
ChromeUtils.defineModuleGetter(this, "FormAutofillNameUtils",
                               "resource://formautofill/FormAutofillNameUtils.jsm");
ChromeUtils.defineModuleGetter(this, "FormAutofillUtils",
                               "resource://formautofill/FormAutofillUtils.jsm");
ChromeUtils.defineModuleGetter(this, "OSKeyStore",
                               "resource://formautofill/OSKeyStore.jsm");
ChromeUtils.defineModuleGetter(this, "PhoneNumber",
                               "resource://formautofill/phonenumberutils/PhoneNumber.jsm");

XPCOMUtils.defineLazyServiceGetter(this, "cryptoSDR",
                                   "@mozilla.org/login-manager/crypto/SDR;1",
                                   Ci.nsILoginManagerCrypto);
XPCOMUtils.defineLazyServiceGetter(this, "gUUIDGenerator",
                                   "@mozilla.org/uuid-generator;1",
                                   "nsIUUIDGenerator");

const CryptoHash = Components.Constructor("@mozilla.org/security/hash;1",
                                          "nsICryptoHash", "initWithString");

const PROFILE_JSON_FILE_NAME = "autofill-profiles.json";

const STORAGE_SCHEMA_VERSION = 1;
const ADDRESS_SCHEMA_VERSION = 1;
const CREDIT_CARD_SCHEMA_VERSION = 2;

const VALID_ADDRESS_FIELDS = [
  "given-name",
  "additional-name",
  "family-name",
  "organization",
  "street-address",
  "address-level3",
  "address-level2",
  "address-level1",
  "postal-code",
  "country",
  "tel",
  "email",
];

const STREET_ADDRESS_COMPONENTS = [
  "address-line1",
  "address-line2",
  "address-line3",
];

const TEL_COMPONENTS = [
  "tel-country-code",
  "tel-national",
  "tel-area-code",
  "tel-local",
  "tel-local-prefix",
  "tel-local-suffix",
];

const VALID_ADDRESS_COMPUTED_FIELDS = [
  "name",
  "country-name",
].concat(STREET_ADDRESS_COMPONENTS, TEL_COMPONENTS);

const VALID_CREDIT_CARD_FIELDS = [
  "billingAddressGUID",
  "cc-name",
  "cc-number",
  "cc-exp-month",
  "cc-exp-year",
  "cc-type",
];

const VALID_CREDIT_CARD_COMPUTED_FIELDS = [
  "cc-given-name",
  "cc-additional-name",
  "cc-family-name",
  "cc-number-encrypted",
  "cc-exp",
];

const INTERNAL_FIELDS = [
  "guid",
  "version",
  "timeCreated",
  "timeLastUsed",
  "timeLastModified",
  "timesUsed",
];

function sha512(string) {
  if (string == null) {
    return null;
  }
  let encoder = new TextEncoder("utf-8");
  let bytes = encoder.encode(string);
  let hash = new CryptoHash("sha512");
  hash.update(bytes, bytes.length);
  return hash.finish(/* base64 */ true);
}

/**
 * Class that manipulates records in a specified collection.
 *
 * Note that it is responsible for converting incoming data to a consistent
 * format in the storage. For example, computed fields will be transformed to
 * the original fields and 2-digit years will be calculated into 4 digits.
 */
class AutofillRecords {
  /**
   * Creates an AutofillRecords.
   *
   * @param {JSONFile} store
   *        An instance of JSONFile.
   * @param {string} collectionName
   *        A key of "store.data".
   * @param {Array.<string>} validFields
   *        A list containing non-metadata field names.
   * @param {Array.<string>} validComputedFields
   *        A list containing computed field names.
   * @param {number} schemaVersion
   *        The schema version for the new record.
   */
  constructor(store, collectionName, validFields, validComputedFields, schemaVersion) {
    FormAutofill.defineLazyLogGetter(this, "AutofillRecords:" + collectionName);

    this.VALID_FIELDS = validFields;
    this.VALID_COMPUTED_FIELDS = validComputedFields;

    this._store = store;
    this._collectionName = collectionName;
    this._schemaVersion = schemaVersion;

    this._initializePromise =
      Promise.all(this._data.map(async (record, index) => this._migrateRecord(record, index)))
        .then(hasChangesArr => {
          let dataHasChanges = hasChangesArr.includes(true);
          if (dataHasChanges) {
            this._store.saveSoon();
          }
        });
  }

  /**
   * Gets the schema version number.
   *
   * @returns {number}
   *          The current schema version number.
   */
  get version() {
    return this._schemaVersion;
  }

  /**
   * Gets the data of this collection.
   *
   * @returns {array}
   *          The data object.
   */
  get _data() {
    return this._store.data[this._collectionName];
  }

  // Ensures that we don't try to apply synced records with newer schema
  // versions. This is a temporary measure to ensure we don't accidentally
  // bump the schema version without a syncing strategy in place (bug 1377204).
  _ensureMatchingVersion(record) {
    if (record.version != this.version) {
      throw new Error(`Got unknown record version ${
        record.version}; want ${this.version}`);
    }
  }

  /**
   * Initialize the records in the collection, resolves when the migration completes.
   * @returns {Promise}
   */
  initialize() {
    return this._initializePromise;
  }

  /**
   * Adds a new record.
   *
   * @param {Object} record
   *        The new record for saving.
   * @param {boolean} [options.sourceSync = false]
   *        Did sync generate this addition?
   * @returns {Promise<string>}
   *          The GUID of the newly added item..
   */
  async add(record, {sourceSync = false} = {}) {
    this.log.debug("add:", record);

    let recordToSave = this._clone(record);

    if (sourceSync) {
      // Remove tombstones for incoming items that were changed on another
      // device. Local deletions always lose to avoid data loss.
      let index = this._findIndexByGUID(recordToSave.guid, {
        includeDeleted: true,
      });
      if (index > -1) {
        let existing = this._data[index];
        if (existing.deleted) {
          this._data.splice(index, 1);
        } else {
          throw new Error(`Record ${recordToSave.guid} already exists`);
        }
      }
    } else if (!recordToSave.deleted) {
      this._normalizeRecord(recordToSave);
      // _normalizeRecord shouldn't do any validation (throw) because in the
      // `update` case it is called with partial records whereas
      // `_validateFields` is called with a complete one.
      this._validateFields(recordToSave);

      recordToSave.guid = this._generateGUID();
      recordToSave.version = this.version;

      // Metadata
      let now = Date.now();
      recordToSave.timeCreated = now;
      recordToSave.timeLastModified = now;
      recordToSave.timeLastUsed = 0;
      recordToSave.timesUsed = 0;
    }

    return this._saveRecord(recordToSave, {sourceSync});
  }

  async _saveRecord(record, {sourceSync = false} = {}) {
    if (!record.guid) {
      throw new Error("Record missing GUID");
    }

    let recordToSave;
    if (record.deleted) {
      if (this._findByGUID(record.guid, {includeDeleted: true})) {
        throw new Error("a record with this GUID already exists");
      }
      recordToSave = {
        guid: record.guid,
        timeLastModified: record.timeLastModified || Date.now(),
        deleted: true,
      };
    } else {
      this._ensureMatchingVersion(record);
      recordToSave = record;
      await this.computeFields(recordToSave);
    }

    if (sourceSync) {
      let sync = this._getSyncMetaData(recordToSave, true);
      sync.changeCounter = 0;
    }

    this._data.push(recordToSave);

    this._store.saveSoon();

    Services.obs.notifyObservers({wrappedJSObject: {
      sourceSync,
      guid: record.guid,
      collectionName: this._collectionName,
    }}, "formautofill-storage-changed", "add");
    return recordToSave.guid;
  }

  _generateGUID() {
    let guid;
    while (!guid || this._findByGUID(guid)) {
      guid = gUUIDGenerator.generateUUID().toString()
                           .replace(/[{}-]/g, "").substring(0, 12);
    }
    return guid;
  }

  /**
   * Update the specified record.
   *
   * @param  {string} guid
   *         Indicates which record to update.
   * @param  {Object} record
   *         The new record used to overwrite the old one.
   * @param  {Promise<boolean>} [preserveOldProperties = false]
   *         Preserve old record's properties if they don't exist in new record.
   */
  async update(guid, record, preserveOldProperties = false) {
    this.log.debug("update:", guid, record);

    let recordFoundIndex = this._findIndexByGUID(guid);
    if (recordFoundIndex == -1) {
      throw new Error("No matching record.");
    }

    // Clone the record before modifying it to avoid exposing incomplete changes.
    let recordFound = this._clone(this._data[recordFoundIndex]);
    await this._stripComputedFields(recordFound);

    let recordToUpdate = this._clone(record);
    this._normalizeRecord(recordToUpdate, true);

    let hasValidField = false;
    for (let field of this.VALID_FIELDS) {
      let oldValue = recordFound[field];
      let newValue = recordToUpdate[field];

      // Resume the old field value in the perserve case
      if (preserveOldProperties && newValue === undefined) {
        newValue = oldValue;
      }

      if (newValue === undefined || newValue === "") {
        delete recordFound[field];
      } else {
        hasValidField = true;
        recordFound[field] = newValue;
      }

      this._maybeStoreLastSyncedField(recordFound, field, oldValue);
    }

    if (!hasValidField) {
      throw new Error("Record contains no valid field.");
    }

    // _normalizeRecord above is called with the `record` argument provided to
    // `update` which may not contain all resulting fields when
    // `preserveOldProperties` is used. This means we need to validate for
    // missing fields after we compose the record (`recordFound`) with the stored
    // record like we do in the loop above.
    this._validateFields(recordFound);

    recordFound.timeLastModified = Date.now();
    let syncMetadata = this._getSyncMetaData(recordFound);
    if (syncMetadata) {
      syncMetadata.changeCounter += 1;
    }

    await this.computeFields(recordFound);
    this._data[recordFoundIndex] = recordFound;

    this._store.saveSoon();

    Services.obs.notifyObservers({wrappedJSObject: {
      guid,
      collectionName: this._collectionName,
    }}, "formautofill-storage-changed", "update");
  }

  /**
   * Notifies the storage of the use of the specified record, so we can update
   * the metadata accordingly. This does not bump the Sync change counter, since
   * we don't sync `timesUsed` or `timeLastUsed`.
   *
   * @param  {string} guid
   *         Indicates which record to be notified.
   */
  notifyUsed(guid) {
    this.log.debug("notifyUsed:", guid);

    let recordFound = this._findByGUID(guid);
    if (!recordFound) {
      throw new Error("No matching record.");
    }

    recordFound.timesUsed++;
    recordFound.timeLastUsed = Date.now();

    this._store.saveSoon();
    Services.obs.notifyObservers({wrappedJSObject: {
      guid,
      collectionName: this._collectionName,
    }}, "formautofill-storage-changed", "notifyUsed");
  }

  /**
   * Removes the specified record. No error occurs if the record isn't found.
   *
   * @param  {string} guid
   *         Indicates which record to remove.
   * @param  {boolean} [options.sourceSync = false]
   *         Did Sync generate this removal?
   */
  remove(guid, {sourceSync = false} = {}) {
    this.log.debug("remove:", guid);

    if (sourceSync) {
      this._removeSyncedRecord(guid);
    } else {
      let index = this._findIndexByGUID(guid, {includeDeleted: false});
      if (index == -1) {
        this.log.warn("attempting to remove non-existing entry", guid);
        return;
      }
      let existing = this._data[index];
      if (existing.deleted) {
        return; // already a tombstone - don't touch it.
      }
      let existingSync = this._getSyncMetaData(existing);
      if (existingSync) {
        // existing sync metadata means it has been synced. This means we must
        // leave a tombstone behind.
        this._data[index] = {
          guid,
          timeLastModified: Date.now(),
          deleted: true,
          _sync: existingSync,
        };
        existingSync.changeCounter++;
      } else {
        // If there's no sync meta-data, this record has never been synced, so
        // we can delete it.
        this._data.splice(index, 1);
      }
    }

    this._store.saveSoon();
    Services.obs.notifyObservers({wrappedJSObject: {
      sourceSync,
      guid,
      collectionName: this._collectionName,
    }}, "formautofill-storage-changed", "remove");
  }

  /**
   * Returns the record with the specified GUID.
   *
   * @param   {string} guid
   *          Indicates which record to retrieve.
   * @param   {boolean} [options.rawData = false]
   *          Returns a raw record without modifications and the computed fields
   *          (this includes private fields)
   * @returns {Promise<Object>}
   *          A clone of the record.
   */
  async get(guid, {rawData = false} = {}) {
    this.log.debug("get:", guid, rawData);

    let recordFound = this._findByGUID(guid);
    if (!recordFound) {
      return null;
    }

    // The record is cloned to avoid accidental modifications from outside.
    let clonedRecord = this._cloneAndCleanUp(recordFound);
    if (rawData) {
      await this._stripComputedFields(clonedRecord);
    } else {
      this._recordReadProcessor(clonedRecord);
    }
    return clonedRecord;
  }

  /**
   * Returns all records.
   *
   * @param   {boolean} [options.rawData = false]
   *          Returns raw records without modifications and the computed fields.
   * @param   {boolean} [options.includeDeleted = false]
   *          Also return any tombstone records.
   * @returns {Promise<Array.<Object>>}
   *          An array containing clones of all records.
   */
  async getAll({rawData = false, includeDeleted = false} = {}) {
    this.log.debug("getAll", rawData, includeDeleted);

    let records = this._data.filter(r => !r.deleted || includeDeleted);
    // Records are cloned to avoid accidental modifications from outside.
    let clonedRecords = records.map(r => this._cloneAndCleanUp(r));
    await Promise.all(clonedRecords.map(async record => {
      if (rawData) {
        await this._stripComputedFields(record);
      } else {
        this._recordReadProcessor(record);
      }
    }));
    return clonedRecords;
  }

  /**
   * Return all saved field names in the collection. This method
   * has to be sync because its caller _updateSavedFieldNames() needs
   * to dispatch content message synchronously.
   *
   * @returns {Set} Set containing saved field names.
   */
  getSavedFieldNames() {
    this.log.debug("getSavedFieldNames");

    let records = this._data.filter(r => !r.deleted);
    records
      .map(record => this._cloneAndCleanUp(record))
      .forEach(record => this._recordReadProcessor(record));

    let fieldNames = new Set();
    for (let record of records) {
      for (let fieldName of Object.keys(record)) {
        if (INTERNAL_FIELDS.includes(fieldName) || !record[fieldName]) {
          continue;
        }
        fieldNames.add(fieldName);
      }
    }

    return fieldNames;
  }

  /**
   * Functions intended to be used in the support of Sync.
   */

  /**
   * Stores a hash of the last synced value for a field in a locally updated
   * record. We use this value to rebuild the shared parent, or base, when
   * reconciling incoming records that may have changed on another device.
   *
   * Storing the hash of the values that we last wrote to the Sync server lets
   * us determine if a remote change conflicts with a local change. If the
   * hashes for the base, current local value, and remote value all differ, we
   * have a conflict.
   *
   * These fields are not themselves synced, and will be removed locally as
   * soon as we have successfully written the record to the Sync server - so
   * it is expected they will not remain for long, as changes which cause a
   * last synced field to be written will itself cause a sync.
   *
   * We also skip this for updates made by Sync, for internal fields, for
   * records that haven't been uploaded yet, and for fields which have already
   * been changed since the last sync.
   *
   * @param   {Object} record
   *          The updated local record.
   * @param   {string} field
   *          The field name.
   * @param   {string} lastSyncedValue
   *          The last synced field value.
   */
  _maybeStoreLastSyncedField(record, field, lastSyncedValue) {
    let sync = this._getSyncMetaData(record);
    if (!sync) {
      // The record hasn't been uploaded yet, so we can't end up with merge
      // conflicts.
      return;
    }
    let alreadyChanged = field in sync.lastSyncedFields;
    if (alreadyChanged) {
      // This field was already changed multiple times since the last sync.
      return;
    }
    let newValue = record[field];
    if (lastSyncedValue != newValue) {
      sync.lastSyncedFields[field] = sha512(lastSyncedValue);
    }
  }

  /**
   * Attempts a three-way merge between a changed local record, an incoming
   * remote record, and the shared parent that we synthesize from the last
   * synced fields - see _maybeStoreLastSyncedField.
   *
   * @param   {Object} strippedLocalRecord
   *          The changed local record, currently in storage. Computed fields
   *          are stripped.
   * @param   {Object} remoteRecord
   *          The remote record.
   * @returns {Object|null}
   *          The merged record, or `null` if there are conflicts and the
   *          records can't be merged.
   */
  _mergeSyncedRecords(strippedLocalRecord, remoteRecord) {
    let sync = this._getSyncMetaData(strippedLocalRecord, true);

    // Copy all internal fields from the remote record. We'll update their
    // values in `_replaceRecordAt`.
    let mergedRecord = {};
    for (let field of INTERNAL_FIELDS) {
      if (remoteRecord[field] != null) {
        mergedRecord[field] = remoteRecord[field];
      }
    }

    for (let field of this.VALID_FIELDS) {
      let isLocalSame = false;
      let isRemoteSame = false;
      if (field in sync.lastSyncedFields) {
        // If the field has changed since the last sync, compare hashes to
        // determine if the local and remote values are different. Hashing is
        // expensive, but we don't expect this to happen frequently.
        let lastSyncedValue = sync.lastSyncedFields[field];
        isLocalSame = lastSyncedValue == sha512(strippedLocalRecord[field]);
        isRemoteSame = lastSyncedValue == sha512(remoteRecord[field]);
      } else {
        // Otherwise, if the field hasn't changed since the last sync, we know
        // it's the same locally.
        isLocalSame = true;
        isRemoteSame = strippedLocalRecord[field] == remoteRecord[field];
      }

      let value;
      if (isLocalSame && isRemoteSame) {
        // Local and remote are the same; doesn't matter which one we pick.
        value = strippedLocalRecord[field];
      } else if (isLocalSame && !isRemoteSame) {
        value = remoteRecord[field];
      } else if (!isLocalSame && isRemoteSame) {
        // We don't need to bump the change counter when taking the local
        // change, because the counter must already be > 0 if we're attempting
        // a three-way merge.
        value = strippedLocalRecord[field];
      } else if (strippedLocalRecord[field] == remoteRecord[field]) {
        // Shared parent doesn't match either local or remote, but the values
        // are identical, so there's no conflict.
        value = strippedLocalRecord[field];
      } else {
        // Both local and remote changed to different values. We'll need to fork
        // the local record to resolve the conflict.
        return null;
      }

      if (value != null) {
        mergedRecord[field] = value;
      }
    }

    return mergedRecord;
  }

  /**
   * Replaces a local record with a remote or merged record, copying internal
   * fields and Sync metadata.
   *
   * @param   {number} index
   * @param   {Object} remoteRecord
   * @param   {Promise<boolean>} [options.keepSyncMetadata = false]
   *          Should we copy Sync metadata? This is true if `remoteRecord` is a
   *          merged record with local changes that we need to upload. Passing
   *          `keepSyncMetadata` retains the record's change counter and
   *          last synced fields, so that we don't clobber the local change if
   *          the sync is interrupted after the record is merged, but before
   *          it's uploaded.
   */
  async _replaceRecordAt(index, remoteRecord, {keepSyncMetadata = false} = {}) {
    let localRecord = this._data[index];
    let newRecord = this._clone(remoteRecord);

    await this._stripComputedFields(newRecord);

    this._data[index] = newRecord;

    if (keepSyncMetadata) {
      // It's safe to move the Sync metadata from the old record to the new
      // record, since we always clone records when we return them, and we
      // never hand out references to the metadata object via public methods.
      newRecord._sync = localRecord._sync;
    } else {
      // As a side effect, `_getSyncMetaData` marks the record as syncing if the
      // existing `localRecord` is a dupe of `remoteRecord`, and we're replacing
      // local with remote.
      let sync = this._getSyncMetaData(newRecord, true);
      sync.changeCounter = 0;
    }

    if (!newRecord.timeCreated ||
        localRecord.timeCreated < newRecord.timeCreated) {
      newRecord.timeCreated = localRecord.timeCreated;
    }

    if (!newRecord.timeLastModified ||
        localRecord.timeLastModified > newRecord.timeLastModified) {
      newRecord.timeLastModified = localRecord.timeLastModified;
    }

    // Copy local-only fields from the existing local record.
    for (let field of ["timeLastUsed", "timesUsed"]) {
      if (localRecord[field] != null) {
        newRecord[field] = localRecord[field];
      }
    }

    await this.computeFields(newRecord);
  }

  /**
   * Clones a local record, giving the clone a new GUID and Sync metadata. The
   * original record remains unchanged in storage.
   *
   * @param   {Object} strippedLocalRecord
   *          The local record. Computed fields are stripped.
   * @returns {string}
   *          A clone of the local record with a new GUID.
   */
  async _forkLocalRecord(strippedLocalRecord) {
    let forkedLocalRecord = this._cloneAndCleanUp(strippedLocalRecord);
    forkedLocalRecord.guid = this._generateGUID();

    // Give the record fresh Sync metadata and bump its change counter as a
    // side effect. This also excludes the forked record from de-duping on the
    // next sync, if the current sync is interrupted before the record can be
    // uploaded.
    this._getSyncMetaData(forkedLocalRecord, true);

    await this.computeFields(forkedLocalRecord);
    this._data.push(forkedLocalRecord);

    return forkedLocalRecord;
  }

  /**
   * Reconciles an incoming remote record into the matching local record. This
   * method is only used by Sync; other callers should use `merge`.
   *
   * @param   {Object} remoteRecord
   *          The incoming record. `remoteRecord` must not be a tombstone, and
   *          must have a matching local record with the same GUID. Use
   *          `add` to insert remote records that don't exist locally, and
   *          `remove` to apply remote tombstones.
   * @returns {Promise<Object>}
   *          A `{forkedGUID}` tuple. `forkedGUID` is `null` if the merge
   *          succeeded without conflicts, or a new GUID referencing the
   *          existing locally modified record if the conflicts could not be
   *          resolved.
   */
  async reconcile(remoteRecord) {
    this._ensureMatchingVersion(remoteRecord);
    if (remoteRecord.deleted) {
      throw new Error(`Can't reconcile tombstone ${remoteRecord.guid}`);
    }

    let localIndex = this._findIndexByGUID(remoteRecord.guid);
    if (localIndex < 0) {
      throw new Error(`Record ${remoteRecord.guid} not found`);
    }

    let localRecord = this._data[localIndex];
    let sync = this._getSyncMetaData(localRecord, true);

    let forkedGUID = null;

    if (sync.changeCounter === 0) {
      // Local not modified. Replace local with remote.
      await this._replaceRecordAt(localIndex, remoteRecord, {
        keepSyncMetadata: false,
      });
    } else {
      let strippedLocalRecord = this._clone(localRecord);
      await this._stripComputedFields(strippedLocalRecord);

      let mergedRecord = this._mergeSyncedRecords(strippedLocalRecord, remoteRecord);
      if (mergedRecord) {
        // Local and remote modified, but we were able to merge. Replace the
        // local record with the merged record.
        await this._replaceRecordAt(localIndex, mergedRecord, {
          keepSyncMetadata: true,
        });
      } else {
        // Merge conflict. Fork the local record, then replace the original
        // with the merged record.
        let forkedLocalRecord = await this._forkLocalRecord(strippedLocalRecord);
        forkedGUID = forkedLocalRecord.guid;
        await this._replaceRecordAt(localIndex, remoteRecord, {
          keepSyncMetadata: false,
        });
      }
    }

    this._store.saveSoon();
    Services.obs.notifyObservers({wrappedJSObject: {
      sourceSync: true,
      guid: remoteRecord.guid,
      forkedGUID,
      collectionName: this._collectionName,
    }}, "formautofill-storage-changed", "reconcile");

    return {forkedGUID};
  }

  _removeSyncedRecord(guid) {
    let index = this._findIndexByGUID(guid, {includeDeleted: true});
    if (index == -1) {
      // Removing a record we don't know about. It may have been synced and
      // removed by another device before we saw it. Store the tombstone in
      // case the server is later wiped and we need to reupload everything.
      let tombstone = {
        guid,
        timeLastModified: Date.now(),
        deleted: true,
      };

      let sync = this._getSyncMetaData(tombstone, true);
      sync.changeCounter = 0;
      this._data.push(tombstone);
      return;
    }

    let existing = this._data[index];
    let sync = this._getSyncMetaData(existing, true);
    if (sync.changeCounter > 0) {
      // Deleting a record with unsynced local changes. To avoid potential
      // data loss, we ignore the deletion in favor of the changed record.
      this.log.info("Ignoring deletion for record with local changes",
                    existing);
      return;
    }

    if (existing.deleted) {
      this.log.info("Ignoring deletion for tombstone", existing);
      return;
    }

    // Removing a record that's not changed locally, and that's not already
    // deleted. Replace the record with a synced tombstone.
    this._data[index] = {
      guid,
      timeLastModified: Date.now(),
      deleted: true,
      _sync: sync,
    };
  }

  /**
   * Provide an object that describes the changes to sync.
   *
   * This is called at the start of the sync process to determine what needs
   * to be updated on the server. As the server is updated, sync will update
   * entries in the returned object, and when sync is complete it will pass
   * the object to pushSyncChanges, which will apply the changes to the store.
   *
   * @returns {object}
   *          An object describing the changes to sync.
   */
  pullSyncChanges() {
    let changes = {};

    let profiles = this._data;
    for (let profile of profiles) {
      let sync = this._getSyncMetaData(profile, true);
      if (sync.changeCounter < 1) {
        if (sync.changeCounter != 0) {
          this.log.error("negative change counter", profile);
        }
        continue;
      }
      changes[profile.guid] = {
        profile,
        counter: sync.changeCounter,
        modified: profile.timeLastModified,
        synced: false,
      };
    }
    this._store.saveSoon();

    return changes;
  }

  /**
   * Apply the metadata changes made by Sync.
   *
   * This is called with metadata about what was synced - see pullSyncChanges.
   *
   * @param {object} changes
   *        The possibly modified object obtained via pullSyncChanges.
   */
  pushSyncChanges(changes) {
    for (let [guid, {counter, synced}] of Object.entries(changes)) {
      if (!synced) {
        continue;
      }
      let recordFound = this._findByGUID(guid, {includeDeleted: true});
      if (!recordFound) {
        this.log.warn("No profile found to persist changes for guid " + guid);
        continue;
      }
      let sync = this._getSyncMetaData(recordFound, true);
      sync.changeCounter = Math.max(0, sync.changeCounter - counter);
      if (sync.changeCounter === 0) {
        // Clear the shared parent fields once we've uploaded all pending
        // changes, since the server now matches what we have locally.
        sync.lastSyncedFields = {};
      }
    }
    this._store.saveSoon();
  }

  /**
   * Reset all sync metadata for all items.
   *
   * This is called when Sync is disconnected from this device. All sync
   * metadata for all items is removed.
   */
  resetSync() {
    for (let record of this._data) {
      delete record._sync;
    }
    // XXX - we should probably also delete all tombstones?
    this.log.info("All sync metadata was reset");
  }

  /**
   * Changes the GUID of an item. This should be called only by Sync. There
   * must be an existing record with oldID and it must never have been synced
   * or an error will be thrown. There must be no existing record with newID.
   *
   * No tombstone will be created for the old GUID - we check it hasn't
   * been synced, so no tombstone is necessary.
   *
   * @param   {string} oldID
   *          GUID of the existing item to change the GUID of.
   * @param   {string} newID
   *          The new GUID for the item.
   */
  changeGUID(oldID, newID) {
    this.log.debug("changeGUID: ", oldID, newID);
    if (oldID == newID) {
      throw new Error("changeGUID: old and new IDs are the same");
    }
    if (this._findIndexByGUID(newID) >= 0) {
      throw new Error("changeGUID: record with destination id exists already");
    }

    let index = this._findIndexByGUID(oldID);
    let profile = this._data[index];
    if (!profile) {
      throw new Error("changeGUID: no source record");
    }
    if (this._getSyncMetaData(profile)) {
      throw new Error("changeGUID: existing record has already been synced");
    }

    profile.guid = newID;

    this._store.saveSoon();
  }

  // Used to get, and optionally create, sync metadata. Brand new records will
  // *not* have sync meta-data - it will be created when they are first
  // synced.
  _getSyncMetaData(record, forceCreate = false) {
    if (!record._sync && forceCreate) {
      // create default metadata and indicate we need to save.
      record._sync = {
        changeCounter: 1,
        lastSyncedFields: {},
      };
      this._store.saveSoon();
    }
    return record._sync;
  }

  /**
   * Finds a local record with matching common fields and a different GUID.
   * Sync uses this method to find and update unsynced local records with
   * fields that match incoming remote records. This avoids creating
   * duplicate profiles with the same information.
   *
   * @param   {Object} remoteRecord
   *          The remote record.
   * @returns {Promise<string|null>}
   *          The GUID of the matching local record, or `null` if no records
   *          match.
   */
  async findDuplicateGUID(remoteRecord) {
    if (!remoteRecord.guid) {
      throw new Error("Record missing GUID");
    }
    this._ensureMatchingVersion(remoteRecord);
    if (remoteRecord.deleted) {
      // Tombstones don't carry enough info to de-dupe, and we should have
      // handled them separately when applying the record.
      throw new Error("Tombstones can't have duplicates");
    }
    let localRecords = this._data;
    for (let localRecord of localRecords) {
      if (localRecord.deleted) {
        continue;
      }
      if (localRecord.guid == remoteRecord.guid) {
        throw new Error(`Record ${remoteRecord.guid} already exists`);
      }
      if (this._getSyncMetaData(localRecord)) {
        // This local record has already been uploaded, so it can't be a dupe of
        // another incoming item.
        continue;
      }

      // Ignore computed fields when matching records as they aren't synced at all.
      let strippedLocalRecord = this._clone(localRecord);
      await this._stripComputedFields(strippedLocalRecord);

      let keys = new Set(Object.keys(remoteRecord));
      for (let key of Object.keys(strippedLocalRecord)) {
        keys.add(key);
      }
      // Ignore internal fields when matching records. Internal fields are synced,
      // but almost certainly have different values than the local record, and
      // we'll update them in `reconcile`.
      for (let field of INTERNAL_FIELDS) {
        keys.delete(field);
      }
      if (!keys.size) {
        // This shouldn't ever happen; a valid record will always have fields
        // that aren't computed or internal. Sync can't do anything about that,
        // so we ignore the dubious local record instead of throwing.
        continue;
      }
      let same = true;
      for (let key of keys) {
        // For now, we ensure that both (or neither) records have the field
        // with matching values. This doesn't account for the version yet
        // (bug 1377204).
        same = key in strippedLocalRecord == key in remoteRecord && strippedLocalRecord[key] == remoteRecord[key];
        if (!same) {
          break;
        }
      }
      if (same) {
        return strippedLocalRecord.guid;
      }
    }
    return null;
  }

  /**
   * Internal helper functions.
   */

  _clone(record) {
    return Object.assign({}, record);
  }

  _cloneAndCleanUp(record) {
    let result = {};
    for (let key in record) {
      // Do not expose hidden fields and fields with empty value (mainly used
      // as placeholders of the computed fields).
      if (!key.startsWith("_") && record[key] !== "") {
        result[key] = record[key];
      }
    }
    return result;
  }

  _findByGUID(guid, {includeDeleted = false} = {}) {
    let found = this._findIndexByGUID(guid, {includeDeleted});
    return found < 0 ? undefined : this._data[found];
  }

  _findIndexByGUID(guid, {includeDeleted = false} = {}) {
    return this._data.findIndex(record => {
      return record.guid == guid && (!record.deleted || includeDeleted);
    });
  }

  async _migrateRecord(record, index) {
    let hasChanges = false;

    if (record.deleted) {
      return hasChanges;
    }

    if (!record.version || isNaN(record.version) || record.version < 1) {
      this.log.warn("Invalid record version:", record.version);

      // Force to run the migration.
      record.version = 0;
    }

    if (record.version < this.version) {
      hasChanges = true;

      record = await this._computeMigratedRecord(record);

      if (record.deleted) {
        // record is deleted by _computeMigratedRecord(),
        // go ahead and put it in the store.
        this._data[index] = record;
        return hasChanges;
      }

      // Compute the computed fields before putting it to store.
      await this.computeFields(record);
      this._data[index] = record;

      return hasChanges;
    }

    hasChanges |= await this.computeFields(record);
    return hasChanges;
  }

  _normalizeRecord(record, preserveEmptyFields = false) {
    this._normalizeFields(record);

    for (let key in record) {
      if (!this.VALID_FIELDS.includes(key)) {
        throw new Error(`"${key}" is not a valid field.`);
      }
      if (typeof record[key] !== "string" &&
          typeof record[key] !== "number") {
        throw new Error(`"${key}" contains invalid data type: ${typeof record[key]}`);
      }
      if (!preserveEmptyFields && record[key] === "") {
        delete record[key];
      }
    }

    if (!Object.keys(record).length) {
      throw new Error("Record contains no valid field.");
    }
  }

  /**
   * Merge the record if storage has multiple mergeable records.
   * @param {Object} targetRecord
   *        The record for merge.
   * @param {boolean} [strict = false]
   *        In strict merge mode, we'll treat the subset record with empty field
   *        as unable to be merged, but mergeable if in non-strict mode.
   * @returns {Array.<string>}
   *          Return an array of the merged GUID string.
   */
  async mergeToStorage(targetRecord, strict = false) {
    let mergedGUIDs = [];
    for (let record of this._data) {
      if (!record.deleted && await this.mergeIfPossible(record.guid, targetRecord, strict)) {
        mergedGUIDs.push(record.guid);
      }
    }
    this.log.debug("Existing records matching and merging count is", mergedGUIDs.length);
    return mergedGUIDs;
  }

  /**
   * Unconditionally remove all data and tombstones for this collection.
   */
  removeAll({sourceSync = false} = {}) {
    this._store.data[this._collectionName] = [];
    this._store.saveSoon();
    Services.obs.notifyObservers({wrappedJSObject: {
      sourceSync,
      collectionName: this._collectionName,
    }}, "formautofill-storage-changed", "removeAll");
  }

  /**
   * Strip the computed fields based on the record version.
   * @param   {Object} record      The record to migrate
   * @returns {Object}             Migrated record.
   *                               Record is always cloned, with version updated,
   *                               with computed fields stripped.
   *                               Could be a tombstone record, if the record
   *                               should be discorded.
   */
  async _computeMigratedRecord(record) {
    if (!record.deleted) {
      record = this._clone(record);
      await this._stripComputedFields(record);
      record.version = this.version;
    }
    return record;
  }

  async _stripComputedFields(record) {
    this.VALID_COMPUTED_FIELDS.forEach(field => delete record[field]);
  }

  // An interface to be inherited.
  _recordReadProcessor(record) {}

  // An interface to be inherited.
  async computeFields(record) {}

  /**
  * An interface to be inherited to mutate the argument to normalize it.
  *
  * @param {object} partialRecord containing the record passed by the consumer of
  *                               storage and in the case of `update` with
  *                               `preserveOldProperties` will only include the
  *                               properties that the user is changing so the
  *                               lack of a field doesn't mean that the record
  *                               won't have that field.
  */
  _normalizeFields(partialRecord) {}

  /**
   * An interface to be inherited to validate that the complete record is
   * consistent and isn't missing required fields. Overrides should throw for
   * invalid records.
   *
   * @param {object} record containing the complete record that would be stored
   *                        if this doesn't throw due to an error.
   * @throws
   */
  _validateFields(record) {}

  // An interface to be inherited.
  async mergeIfPossible(guid, record, strict) {}
}

class Addresses extends AutofillRecords {
  constructor(store) {
    super(store, "addresses", VALID_ADDRESS_FIELDS, VALID_ADDRESS_COMPUTED_FIELDS, ADDRESS_SCHEMA_VERSION);
  }

  _recordReadProcessor(address) {
    if (address.country && !FormAutofill.countries.has(address.country)) {
      delete address.country;
      delete address["country-name"];
    }
  }

  async computeFields(address) {
    // NOTE: Remember to bump the schema version number if any of the existing
    //       computing algorithm changes. (No need to bump when just adding new
    //       computed fields.)

    // NOTE: Computed fields should be always present in the storage no matter
    //       it's empty or not.

    let hasNewComputedFields = false;

    if (address.deleted) {
      return hasNewComputedFields;
    }

    // Compute name
    if (!("name" in address)) {
      let name = FormAutofillNameUtils.joinNameParts({
        given: address["given-name"],
        middle: address["additional-name"],
        family: address["family-name"],
      });
      address.name = name;
      hasNewComputedFields = true;
    }

    // Compute address lines
    if (!("address-line1" in address)) {
      let streetAddress = [];
      if (address["street-address"]) {
        streetAddress = address["street-address"].split("\n").map(s => s.trim());
      }
      for (let i = 0; i < 3; i++) {
        address["address-line" + (i + 1)] = streetAddress[i] || "";
      }
      if (streetAddress.length > 3) {
        address["address-line3"] = FormAutofillUtils.toOneLineAddress(
          streetAddress.splice(2)
        );
      }
      hasNewComputedFields = true;
    }

    // Compute country name
    if (!("country-name" in address)) {
      if (address.country) {
        try {
          address["country-name"] = Services.intl.getRegionDisplayNames(undefined, [address.country]);
        } catch (e) {
          address["country-name"] = "";
        }
      } else {
        address["country-name"] = "";
      }
      hasNewComputedFields = true;
    }

    // Compute tel
    if (!("tel-national" in address)) {
      if (address.tel) {
        let tel = PhoneNumber.Parse(address.tel, address.country || FormAutofill.DEFAULT_REGION);
        if (tel) {
          if (tel.countryCode) {
            address["tel-country-code"] = tel.countryCode;
          }
          if (tel.nationalNumber) {
            address["tel-national"] = tel.nationalNumber;
          }

          // PhoneNumberUtils doesn't support parsing the components of a telephone
          // number so we hard coded the parser for US numbers only. We will need
          // to figure out how to parse numbers from other regions when we support
          // new countries in the future.
          if (tel.nationalNumber && tel.countryCode == "+1") {
            let telComponents = tel.nationalNumber.match(/(\d{3})((\d{3})(\d{4}))$/);
            if (telComponents) {
              address["tel-area-code"] = telComponents[1];
              address["tel-local"] = telComponents[2];
              address["tel-local-prefix"] = telComponents[3];
              address["tel-local-suffix"] = telComponents[4];
            }
          }
        } else {
          // Treat "tel" as "tel-national" directly if it can't be parsed.
          address["tel-national"] = address.tel;
        }
      }

      TEL_COMPONENTS.forEach(c => {
        address[c] = address[c] || "";
      });
    }

    return hasNewComputedFields;
  }

  _normalizeFields(address) {
    this._normalizeName(address);
    this._normalizeAddress(address);
    this._normalizeCountry(address);
    this._normalizeTel(address);
  }

  _normalizeName(address) {
    if (address.name) {
      let nameParts = FormAutofillNameUtils.splitName(address.name);
      if (!address["given-name"] && nameParts.given) {
        address["given-name"] = nameParts.given;
      }
      if (!address["additional-name"] && nameParts.middle) {
        address["additional-name"] = nameParts.middle;
      }
      if (!address["family-name"] && nameParts.family) {
        address["family-name"] = nameParts.family;
      }
    }
    delete address.name;
  }

  _normalizeAddress(address) {
    if (STREET_ADDRESS_COMPONENTS.some(c => !!address[c])) {
      // Treat "street-address" as "address-line1" if it contains only one line
      // and "address-line1" is omitted.
      if (!address["address-line1"] && address["street-address"] &&
          !address["street-address"].includes("\n")) {
        address["address-line1"] = address["street-address"];
        delete address["street-address"];
      }

      // Concatenate "address-line*" if "street-address" is omitted.
      if (!address["street-address"]) {
        address["street-address"] = STREET_ADDRESS_COMPONENTS.map(c => address[c]).join("\n").replace(/\n+$/, "");
      }
    }
    STREET_ADDRESS_COMPONENTS.forEach(c => delete address[c]);
  }

  _normalizeCountry(address) {
    let country;

    if (address.country) {
      country = address.country.toUpperCase();
    } else if (address["country-name"]) {
      country = FormAutofillUtils.identifyCountryCode(address["country-name"]);
    }

    // Only values included in the region list will be saved.
    let hasLocalizedName = false;
    try {
      if (country) {
        let localizedName = Services.intl.getRegionDisplayNames(undefined, [country]);
        hasLocalizedName = localizedName != country;
      }
    } catch (e) {}

    if (country && hasLocalizedName) {
      address.country = country;
    } else {
      delete address.country;
    }

    delete address["country-name"];
  }

  _normalizeTel(address) {
    if (address.tel || TEL_COMPONENTS.some(c => !!address[c])) {
      FormAutofillUtils.compressTel(address);

      let possibleRegion = address.country || FormAutofill.DEFAULT_REGION;
      let tel = PhoneNumber.Parse(address.tel, possibleRegion);

      if (tel && tel.internationalNumber) {
        // Force to save numbers in E.164 format if parse success.
        address.tel = tel.internationalNumber;
      }
    }
    TEL_COMPONENTS.forEach(c => delete address[c]);
  }

  /**
   * Merge new address into the specified address if mergeable.
   *
   * @param  {string} guid
   *         Indicates which address to merge.
   * @param  {Object} address
   *         The new address used to merge into the old one.
   * @param  {boolean} strict
   *         In strict merge mode, we'll treat the subset record with empty field
   *         as unable to be merged, but mergeable if in non-strict mode.
   * @returns {Promise<boolean>}
   *          Return true if address is merged into target with specific guid or false if not.
   */
  async mergeIfPossible(guid, address, strict) {
    this.log.debug("mergeIfPossible:", guid, address);

    let addressFound = this._findByGUID(guid);
    if (!addressFound) {
      throw new Error("No matching address.");
    }

    let addressToMerge = this._clone(address);
    this._normalizeRecord(addressToMerge, strict);
    let hasMatchingField = false;

    for (let field of this.VALID_FIELDS) {
      let existingField = addressFound[field];
      let incomingField = addressToMerge[field];
      if (incomingField !== undefined && existingField !== undefined) {
        if (incomingField != existingField) {
          // Treat "street-address" as mergeable if their single-line versions
          // match each other.
          if (field == "street-address" &&
              FormAutofillUtils.toOneLineAddress(existingField) == FormAutofillUtils.toOneLineAddress(incomingField)) {
            // Keep the value in storage if its amount of lines is greater than
            // or equal to the incoming one.
            if (existingField.split("\n").length >= incomingField.split("\n").length) {
              // Replace the incoming field with the one in storage so it will
              // be further merged back to storage.
              addressToMerge[field] = existingField;
            }
          } else {
            this.log.debug("Conflicts: field", field, "has different value.");
            return false;
          }
        }
        hasMatchingField = true;
      }
    }

    // We merge the address only when at least one field has the same value.
    if (!hasMatchingField) {
      this.log.debug("Unable to merge because no field has the same value");
      return false;
    }

    // Early return if the data is the same or subset.
    let noNeedToUpdate = this.VALID_FIELDS.every((field) => {
      // When addressFound doesn't contain a field, it's unnecessary to update
      // if the same field in addressToMerge is omitted or an empty string.
      if (addressFound[field] === undefined) {
        return !addressToMerge[field];
      }

      // When addressFound contains a field, it's unnecessary to update if
      // the same field in addressToMerge is omitted or a duplicate.
      return (addressToMerge[field] === undefined) ||
             (addressFound[field] === addressToMerge[field]);
    });
    if (noNeedToUpdate) {
      return true;
    }

    await this.update(guid, addressToMerge, true);
    return true;
  }
}

class CreditCards extends AutofillRecords {
  constructor(store) {
    super(store, "creditCards", VALID_CREDIT_CARD_FIELDS, VALID_CREDIT_CARD_COMPUTED_FIELDS, CREDIT_CARD_SCHEMA_VERSION);
  }

  async computeFields(creditCard) {
    // NOTE: Remember to bump the schema version number if any of the existing
    //       computing algorithm changes. (No need to bump when just adding new
    //       computed fields.)

    // NOTE: Computed fields should be always present in the storage no matter
    //       it's empty or not.

    let hasNewComputedFields = false;

    if (creditCard.deleted) {
      return hasNewComputedFields;
    }

    // Compute split names
    if (!("cc-given-name" in creditCard)) {
      let nameParts = FormAutofillNameUtils.splitName(creditCard["cc-name"]);
      creditCard["cc-given-name"] = nameParts.given;
      creditCard["cc-additional-name"] = nameParts.middle;
      creditCard["cc-family-name"] = nameParts.family;
      hasNewComputedFields = true;
    }

    // Compute credit card expiration date
    if (!("cc-exp" in creditCard)) {
      if (creditCard["cc-exp-month"] && creditCard["cc-exp-year"]) {
        creditCard["cc-exp"] = String(creditCard["cc-exp-year"]) + "-" + String(creditCard["cc-exp-month"]).padStart(2, "0");
      } else {
        creditCard["cc-exp"] = "";
      }
      hasNewComputedFields = true;
    }

    // Encrypt credit card number
    if (!("cc-number-encrypted" in creditCard)) {
      if ("cc-number" in creditCard) {
        let ccNumber = creditCard["cc-number"];
        if (CreditCard.isValidNumber(ccNumber)) {
          creditCard["cc-number"] = CreditCard.getLongMaskedNumber(ccNumber);
        } else {
          // Credit card numbers can be entered on versions of Firefox that don't validate
          // the number and then synced to this version of Firefox. Therefore, mask the
          // full number if the number is invalid on this version.
          creditCard["cc-number"] = "*".repeat(ccNumber.length);
        }
        creditCard["cc-number-encrypted"] = await OSKeyStore.encrypt(ccNumber);
      } else {
        creditCard["cc-number-encrypted"] = "";
      }
    }

    return hasNewComputedFields;
  }

  async _computeMigratedRecord(creditCard) {
    if (creditCard["cc-number-encrypted"]) {
      switch (creditCard.version) {
        case 1: {
          if (!cryptoSDR.isLoggedIn) {
            // We cannot decrypt the data, so silently remove the record for
            // the user.
            if (creditCard.deleted) {
              break;
            }

            this.log.warn("Removing version 1 credit card record to migrate to new encryption:", creditCard.guid);

            // Replace the record with a tombstone record here,
            // regardless of existence of sync metadata.
            let existingSync = this._getSyncMetaData(creditCard);
            creditCard = {
              guid: creditCard.guid,
              timeLastModified: Date.now(),
              deleted: true,
            };

            if (existingSync) {
              creditCard._sync = existingSync;
              existingSync.changeCounter++;
            }
            break;
          }

          creditCard = this._clone(creditCard);

          // Decrypt the cc-number using version 1 encryption.
          let ccNumber = cryptoSDR.decrypt(creditCard["cc-number-encrypted"]);
          // Re-encrypt the cc-number with version 2 encryption.
          creditCard["cc-number-encrypted"] = await OSKeyStore.encrypt(ccNumber);
          break;
        }

        default:
          throw new Error("Unknown credit card version to migrate: " + creditCard.version);
      }
    }
    return super._computeMigratedRecord(creditCard);
  }

  async _stripComputedFields(creditCard) {
    if (creditCard["cc-number-encrypted"]) {
      try {
        creditCard["cc-number"] = await OSKeyStore.decrypt(creditCard["cc-number-encrypted"]);
      } catch (ex) {
        if (ex.result == Cr.NS_ERROR_ABORT) {
          throw ex;
        }
        // Quietly recover from encryption error,
        // so existing credit card entry with undecryptable number
        // can be updated.
      }
    }
    await super._stripComputedFields(creditCard);
  }

  _normalizeFields(creditCard) {
    this._normalizeCCName(creditCard);
    this._normalizeCCNumber(creditCard);
    this._normalizeCCExpirationDate(creditCard);
  }

  _normalizeCCName(creditCard) {
    if (creditCard["cc-given-name"] || creditCard["cc-additional-name"] || creditCard["cc-family-name"]) {
      if (!creditCard["cc-name"]) {
        creditCard["cc-name"] = FormAutofillNameUtils.joinNameParts({
          given: creditCard["cc-given-name"],
          middle: creditCard["cc-additional-name"],
          family: creditCard["cc-family-name"],
        });
      }
    }
    delete creditCard["cc-given-name"];
    delete creditCard["cc-additional-name"];
    delete creditCard["cc-family-name"];
  }

  _normalizeCCNumber(creditCard) {
    if (!("cc-number" in creditCard)) {
      return;
    }
    if (!CreditCard.isValidNumber(creditCard["cc-number"])) {
      delete creditCard["cc-number"];
      return;
    }
    let card = new CreditCard({number: creditCard["cc-number"]});
    creditCard["cc-number"] = card.number;
  }

  _normalizeCCExpirationDate(creditCard) {
    let normalizedExpiration = CreditCard.normalizeExpiration({
      expirationMonth: creditCard["cc-exp-month"],
      expirationYear: creditCard["cc-exp-year"],
      expirationString: creditCard["cc-exp"],
    });
    if (normalizedExpiration.month) {
      creditCard["cc-exp-month"] = normalizedExpiration.month;
    } else {
      delete creditCard["cc-exp-month"];
    }
    if (normalizedExpiration.year) {
      creditCard["cc-exp-year"] = normalizedExpiration.year;
    } else {
      delete creditCard["cc-exp-year"];
    }
    delete creditCard["cc-exp"];
  }

  _validateFields(creditCard) {
    if (!creditCard["cc-number"]) {
      throw new Error("Missing/invalid cc-number");
    }
  }

  _ensureMatchingVersion(record) {
    if (!record.version || isNaN(record.version) || record.version < 1) {
      throw new Error(`Got invalid record version ${
        record.version}; want ${this.version}`);
    }

    if (record.version < this.version) {
      switch (record.version) {
        case 1:
          // The difference between version 1 and 2 is only about the encryption
          // method used for the cc-number-encrypted field.
          // As long as the record is already decrypted, it is safe to bump the
          // version directly.
          if (!record["cc-number-encrypted"]) {
            record.version = this.version;
          } else {
            throw new Error("Unexpected record migration path.");
          }
          break;
        default:
          throw new Error("Unknown credit card version to match: " + record.version);
      }
    }

    return super._ensureMatchingVersion(record);
  }

  /**
   * Normalize the given record and return the first matched guid if storage has the same record.
   * @param {Object} targetCreditCard
   *        The credit card for duplication checking.
   * @returns {Promise<string|null>}
   *          Return the first guid if storage has the same credit card and null otherwise.
   */
  async getDuplicateGuid(targetCreditCard) {
    let clonedTargetCreditCard = this._clone(targetCreditCard);
    this._normalizeRecord(clonedTargetCreditCard);
    for (let creditCard of this._data) {
      let isDuplicate = await Promise.all(this.VALID_FIELDS.map(async field => {
        if (!clonedTargetCreditCard[field]) {
          return !creditCard[field];
        }
        if (field == "cc-number" && creditCard[field]) {
          // Compare the masked numbers instead when decryption requires a password
          // because we don't want to leak the credit card number.
          return CreditCard.getLongMaskedNumber(clonedTargetCreditCard[field]) == creditCard[field];
        }
        return clonedTargetCreditCard[field] == creditCard[field];
      })).then(fieldResults => fieldResults.every(result => result));
      if (isDuplicate) {
        return creditCard.guid;
      }
    }
    return null;
  }

  /**
   * Merge new credit card into the specified record if cc-number is identical.
   * (Note that credit card records always do non-strict merge.)
   *
   * @param  {string} guid
   *         Indicates which credit card to merge.
   * @param  {Object} creditCard
   *         The new credit card used to merge into the old one.
   * @returns {boolean}
   *          Return true if credit card is merged into target with specific guid or false if not.
   */
  async mergeIfPossible(guid, creditCard) {
    this.log.debug("mergeIfPossible:", guid, creditCard);

    // Credit card number is required since it also must match.
    if (!creditCard["cc-number"]) {
      return false;
    }

    // Query raw data for comparing the decrypted credit card number
    let creditCardFound = await this.get(guid, {rawData: true});
    if (!creditCardFound) {
      throw new Error("No matching credit card.");
    }

    let creditCardToMerge = this._clone(creditCard);
    this._normalizeRecord(creditCardToMerge);

    for (let field of this.VALID_FIELDS) {
      let existingField = creditCardFound[field];

      // Make sure credit card field is existed and have value
      if (field == "cc-number" && (!existingField || !creditCardToMerge[field])) {
        return false;
      }

      if (!creditCardToMerge[field] && typeof(existingField) != "undefined") {
        creditCardToMerge[field] = existingField;
      }

      let incomingField = creditCardToMerge[field];
      if (incomingField && existingField) {
        if (incomingField != existingField) {
          this.log.debug("Conflicts: field", field, "has different value.");
          return false;
        }
      }
    }

    // Early return if the data is the same.
    let exactlyMatch = this.VALID_FIELDS.every((field) =>
      creditCardFound[field] === creditCardToMerge[field]
    );
    if (exactlyMatch) {
      return true;
    }

    await this.update(guid, creditCardToMerge, true);
    return true;
  }
}

function FormAutofillStorage(path) {
  this._path = path;
  this._initializePromise = null;
  this.INTERNAL_FIELDS = INTERNAL_FIELDS;
}

FormAutofillStorage.prototype = {
  get version() {
    return STORAGE_SCHEMA_VERSION;
  },

  get addresses() {
    if (!this._addresses) {
      this._store.ensureDataReady();
      this._addresses = new Addresses(this._store);
    }
    return this._addresses;
  },

  get creditCards() {
    if (!this._creditCards) {
      this._store.ensureDataReady();
      this._creditCards = new CreditCards(this._store);
    }
    return this._creditCards;
  },

  /**
   * Loads the profile data from file to memory.
   *
   * @returns {Promise}
   * @resolves When the operation finished successfully.
   * @rejects  JavaScript exception.
   */
  initialize() {
    if (!this._initializePromise) {
      this._store = new JSONFile({
        path: this._path,
        dataPostProcessor: this._dataPostProcessor.bind(this),
      });
      this._initializePromise = this._store.load()
        .then(() => {
          let initializeAutofillRecords = [this.addresses.initialize()];
          if (FormAutofill.isAutofillCreditCardsAvailable) {
            initializeAutofillRecords.push(this.creditCards.initialize());
          } else {
            // Make creditCards records unavailable to other modules
            // because we never initialize it.
            Object.defineProperty(this, "creditCards", {
              get() {
                throw new Error("CreditCards is not initialized. " +
                                "Please restart if you flip the pref manually.");
              },
            });
          }
          return Promise.all(initializeAutofillRecords);
        });
    }
    return this._initializePromise;
  },

  _dataPostProcessor(data) {
    data.version = this.version;
    if (!data.addresses) {
      data.addresses = [];
    }
    if (!data.creditCards) {
      data.creditCards = [];
    }
    return data;
  },

  // For test only.
  _saveImmediately() {
    return this._store._save();
  },

  _finalize() {
    return this._store.finalize();
  },
};

// The singleton exposed by this module.
this.formAutofillStorage = new FormAutofillStorage(
  OS.Path.join(OS.Constants.Path.profileDir, PROFILE_JSON_FILE_NAME));
PK
!<юP%--chrome/res/FormAutofillSync.jsm/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

var EXPORTED_SYMBOLS = ["AddressesEngine", "CreditCardsEngine"];

ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://services-sync/engines.js");
ChromeUtils.import("resource://services-sync/record.js");
ChromeUtils.import("resource://services-sync/util.js");
ChromeUtils.import("resource://services-sync/constants.js");
ChromeUtils.import("resource://formautofill/FormAutofillUtils.jsm");

ChromeUtils.defineModuleGetter(this, "Log",
                               "resource://gre/modules/Log.jsm");
ChromeUtils.defineModuleGetter(this, "formAutofillStorage",
                               "resource://formautofill/FormAutofillStorage.jsm");

// A helper to sanitize address and creditcard records suitable for logging.
function sanitizeStorageObject(ob) {
  if (!ob) {
    return null;
  }
  const whitelist = ["timeCreated", "timeLastUsed", "timeLastModified"];
  let result = {};
  for (let key of Object.keys(ob)) {
    let origVal = ob[key];
    if (whitelist.includes(key)) {
      result[key] = origVal;
    } else if (typeof origVal == "string") {
      result[key] = "X".repeat(origVal.length);
    } else {
      result[key] = typeof(origVal); // *shrug*
    }
  }
  return result;
}


function AutofillRecord(collection, id) {
  CryptoWrapper.call(this, collection, id);
}

AutofillRecord.prototype = {
  __proto__: CryptoWrapper.prototype,

  toEntry() {
    return Object.assign({
      guid: this.id,
    }, this.entry);
  },

  fromEntry(entry) {
    this.id = entry.guid;
    this.entry = entry;
    // The GUID is already stored in record.id, so we nuke it from the entry
    // itself to save a tiny bit of space. The formAutofillStorage clones profiles,
    // so nuking in-place is OK.
    delete this.entry.guid;
  },

  cleartextToString() {
    // And a helper so logging a *Sync* record auto sanitizes.
    let record = this.cleartext;
    return JSON.stringify({entry: sanitizeStorageObject(record.entry)});
  },
};

// Profile data is stored in the "entry" object of the record.
Utils.deferGetSet(AutofillRecord, "cleartext", ["entry"]);

function FormAutofillStore(name, engine) {
  Store.call(this, name, engine);
}

FormAutofillStore.prototype = {
  __proto__: Store.prototype,

  _subStorageName: null, // overridden below.
  _storage: null,

  get storage() {
    if (!this._storage) {
      this._storage = formAutofillStorage[this._subStorageName];
    }
    return this._storage;
  },

  async getAllIDs() {
    let result = {};
    for (let {guid} of await this.storage.getAll({includeDeleted: true})) {
      result[guid] = true;
    }
    return result;
  },

  async changeItemID(oldID, newID) {
    this.storage.changeGUID(oldID, newID);
  },

  // Note: this function intentionally returns false in cases where we only have
  // a (local) tombstone - and formAutofillStorage.get() filters them for us.
  async itemExists(id) {
    return Boolean(await this.storage.get(id));
  },

  async applyIncoming(remoteRecord) {
    if (remoteRecord.deleted) {
      this._log.trace("Deleting record", remoteRecord);
      this.storage.remove(remoteRecord.id, {sourceSync: true});
      return;
    }

    if (await this.itemExists(remoteRecord.id)) {
      // We will never get a tombstone here, so we are updating a real record.
      await this._doUpdateRecord(remoteRecord);
      return;
    }

    // No matching local record. Try to dedupe a NEW local record.
    let localDupeID = await this.storage.findDuplicateGUID(remoteRecord.toEntry());
    if (localDupeID) {
      this._log.trace(`Deduping local record ${localDupeID} to remote`, remoteRecord);
      // Change the local GUID to match the incoming record, then apply the
      // incoming record.
      await this.changeItemID(localDupeID, remoteRecord.id);
      await this._doUpdateRecord(remoteRecord);
      return;
    }

    // We didn't find a dupe, either, so must be a new record (or possibly
    // a non-deleted version of an item we have a tombstone for, which add()
    // handles for us.)
    this._log.trace("Add record", remoteRecord);
    let entry = remoteRecord.toEntry();
    await this.storage.add(entry, {sourceSync: true});
  },

  async createRecord(id, collection) {
    this._log.trace("Create record", id);
    let record = new AutofillRecord(collection, id);
    let entry = await this.storage.get(id, {
      rawData: true,
    });
    if (entry) {
      record.fromEntry(entry);
    } else {
      // We should consider getting a more authortative indication it's actually deleted.
      this._log.debug(`Failed to get autofill record with id "${id}", assuming deleted`);
      record.deleted = true;
    }
    return record;
  },

  async _doUpdateRecord(record) {
    this._log.trace("Updating record", record);

    let entry = record.toEntry();
    let {forkedGUID} = await this.storage.reconcile(entry);
    if (this._log.level <= Log.Level.Debug) {
      let forkedRecord = forkedGUID ? await this.storage.get(forkedGUID) : null;
      let reconciledRecord = await this.storage.get(record.id);
      this._log.debug("Updated local record", {
        forked: sanitizeStorageObject(forkedRecord),
        updated: sanitizeStorageObject(reconciledRecord),
      });
    }
  },

  // NOTE: Because we re-implement the incoming/reconcilliation logic we leave
  // the |create|, |remove| and |update| methods undefined - the base
  // implementation throws, which is what we want to happen so we can identify
  // any places they are "accidentally" called.
};

function FormAutofillTracker(name, engine) {
  Tracker.call(this, name, engine);
}

FormAutofillTracker.prototype = {
  __proto__: Tracker.prototype,
  async observe(subject, topic, data) {
    if (topic != "formautofill-storage-changed") {
      return;
    }
    if (subject && subject.wrappedJSObject && subject.wrappedJSObject.sourceSync) {
      return;
    }
    switch (data) {
      case "add":
      case "update":
      case "remove":
        this.score += SCORE_INCREMENT_XLARGE;
        break;
      default:
        this._log.debug("unrecognized autofill notification", data);
        break;
    }
  },

  // `_ignore` checks the change source for each observer notification, so we
  // don't want to let the engine ignore all changes during a sync.
  get ignoreAll() {
    return false;
  },

  // Define an empty setter so that the engine doesn't throw a `TypeError`
  // setting a read-only property.
  set ignoreAll(value) {},

  onStart() {
    Services.obs.addObserver(this, "formautofill-storage-changed");
  },

  onStop() {
    Services.obs.removeObserver(this, "formautofill-storage-changed");
  },

  // We never want to persist changed IDs, as the changes are already stored
  // in FormAutofillStorage
  persistChangedIDs: false,

  // Ensure we aren't accidentally using the base persistence.
  get changedIDs() {
    throw new Error("changedIDs isn't meaningful for this engine");
  },

  set changedIDs(obj) {
    throw new Error("changedIDs isn't meaningful for this engine");
  },

  addChangedID(id, when) {
    throw new Error("Don't add IDs to the autofill tracker");
  },

  removeChangedID(id) {
    throw new Error("Don't remove IDs from the autofill tracker");
  },

  // This method is called at various times, so we override with a no-op
  // instead of throwing.
  clearChangedIDs() {},
};

// This uses the same conventions as BookmarkChangeset in
// services/sync/modules/engines/bookmarks.js. Specifically,
// - "synced" means the item has already been synced (or we have another reason
//   to ignore it), and should be ignored in most methods.
class AutofillChangeset extends Changeset {
  constructor() {
    super();
  }

  getModifiedTimestamp(id) {
    throw new Error("Don't use timestamps to resolve autofill merge conflicts");
  }

  has(id) {
    let change = this.changes[id];
    if (change) {
      return !change.synced;
    }
    return false;
  }

  delete(id) {
    let change = this.changes[id];
    if (change) {
      // Mark the change as synced without removing it from the set. We do this
      // so that we can update FormAutofillStorage in `trackRemainingChanges`.
      change.synced = true;
    }
  }
}

function FormAutofillEngine(service, name) {
  SyncEngine.call(this, name, service);
}

FormAutofillEngine.prototype = {
  __proto__: SyncEngine.prototype,

  // the priority for this engine is == addons, so will happen after bookmarks
  // prefs and tabs, but before forms, history, etc.
  syncPriority: 5,

  // We don't use SyncEngine.initialize() for this, as we initialize even if
  // the engine is disabled, and we don't want to be the loader of
  // FormAutofillStorage in this case.
  async _syncStartup() {
    await formAutofillStorage.initialize();
    await SyncEngine.prototype._syncStartup.call(this);
  },

  // We handle reconciliation in the store, not the engine.
  async _reconcile() {
    return true;
  },

  emptyChangeset() {
    return new AutofillChangeset();
  },

  async _uploadOutgoing() {
    this._modified.replace(this._store.storage.pullSyncChanges());
    await SyncEngine.prototype._uploadOutgoing.call(this);
  },

  // Typically, engines populate the changeset before downloading records.
  // However, we handle conflict resolution in the store, so we can wait
  // to pull changes until we're ready to upload.
  async pullAllChanges() {
    return {};
  },

  async pullNewChanges() {
    return {};
  },

  async trackRemainingChanges() {
    this._store.storage.pushSyncChanges(this._modified.changes);
  },

  _deleteId(id) {
    this._noteDeletedId(id);
  },

  async _resetClient() {
    await formAutofillStorage.initialize();
    this._store.storage.resetSync();
  },

  async _wipeClient() {
    await formAutofillStorage.initialize();
    this._store.storage.removeAll({sourceSync: true});
  },

};

// The concrete engines

function AddressesRecord(collection, id) {
  AutofillRecord.call(this, collection, id);
}

AddressesRecord.prototype = {
  __proto__: AutofillRecord.prototype,
  _logName: "Sync.Record.Addresses",
};

function AddressesStore(name, engine) {
  FormAutofillStore.call(this, name, engine);
}

AddressesStore.prototype = {
  __proto__: FormAutofillStore.prototype,
  _subStorageName: "addresses",
};

function AddressesEngine(service) {
  FormAutofillEngine.call(this, service, "Addresses");
}

AddressesEngine.prototype = {
  __proto__: FormAutofillEngine.prototype,
  _trackerObj: FormAutofillTracker,
  _storeObj: AddressesStore,
  _recordObj: AddressesRecord,

  get prefName() {
    return "addresses";
  },
};

function CreditCardsRecord(collection, id) {
  AutofillRecord.call(this, collection, id);
}

CreditCardsRecord.prototype = {
  __proto__: AutofillRecord.prototype,
  _logName: "Sync.Record.CreditCards",
};

function CreditCardsStore(name, engine) {
  FormAutofillStore.call(this, name, engine);
}

CreditCardsStore.prototype = {
  __proto__: FormAutofillStore.prototype,
  _subStorageName: "creditCards",
};

function CreditCardsEngine(service) {
  FormAutofillEngine.call(this, service, "CreditCards");
}

CreditCardsEngine.prototype = {
  __proto__: FormAutofillEngine.prototype,
  _trackerObj: FormAutofillTracker,
  _storeObj: CreditCardsStore,
  _recordObj: CreditCardsRecord,
  get prefName() {
    return "creditcards";
  },
};
PK
!<%—U'¯w¯w chrome/res/FormAutofillUtils.jsm/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

var EXPORTED_SYMBOLS = ["FormAutofillUtils", "AddressDataLoader"];

const ADDRESS_METADATA_PATH = "resource://formautofill/addressmetadata/";
const ADDRESS_REFERENCES = "addressReferences.js";
const ADDRESS_REFERENCES_EXT = "addressReferencesExt.js";

const ADDRESSES_COLLECTION_NAME = "addresses";
const CREDITCARDS_COLLECTION_NAME = "creditCards";
const MANAGE_ADDRESSES_KEYWORDS = ["manageAddressesTitle", "addNewAddressTitle"];
const EDIT_ADDRESS_KEYWORDS = [
  "givenName", "additionalName", "familyName", "organization2", "streetAddress",
  "state", "province", "city", "country", "zip", "postalCode", "email", "tel",
];
const MANAGE_CREDITCARDS_KEYWORDS = ["manageCreditCardsTitle", "addNewCreditCardTitle"];
const EDIT_CREDITCARD_KEYWORDS = ["cardNumber", "nameOnCard", "cardExpiresMonth", "cardExpiresYear", "cardNetwork"];
const FIELD_STATES = {
  NORMAL: "NORMAL",
  AUTO_FILLED: "AUTO_FILLED",
  PREVIEW: "PREVIEW",
};
const SECTION_TYPES = {
  ADDRESS: "address",
  CREDIT_CARD: "creditCard",
};

// The maximum length of data to be saved in a single field for preventing DoS
// attacks that fill the user's hard drive(s).
const MAX_FIELD_VALUE_LENGTH = 200;

ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://formautofill/FormAutofill.jsm");
ChromeUtils.defineModuleGetter(this, "CreditCard",
  "resource://gre/modules/CreditCard.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) {
        let addressDataForKey = sandbox.addressData[key];
        if (!addressDataForKey) {
          addressDataForKey = sandbox.addressData[key] = {};
        }

        Object.assign(addressDataForKey, extSandbox.addressDataExt[key]);
      }
    }
    return sandbox;
  },

  /**
   * Convert certain properties' string value into array. We should make sure
   * the cached data is parsed.
   * @param   {object} data Original metadata from addressReferences.
   * @returns {object} parsed metadata with property value that converts to array.
   */
  _parse(data) {
    if (!data) {
      return null;
    }

    const properties = ["languages", "sub_keys", "sub_names", "sub_lnames"];
    for (let key of properties) {
      if (!data[key]) {
        continue;
      }
      // No need to normalize data if the value is array already.
      if (Array.isArray(data[key])) {
        return data;
      }

      data[key] = data[key].split("~");
    }
    return data;
  },

  /**
   * 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} Default locale metadata
   */
  _loadData(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._parse(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._parse(this._addressData[`data/${country}/${level1}`]);
  },

  /**
   * Return the region metadata with default locale and other locales (if exists).
   * @param   {string} country
   * @param   {string?} level1
   * @returns {object} Return default locale and other locales metadata.
   */
  getData(country, level1 = null) {
    let defaultLocale = this._loadData(country, level1);
    if (!defaultLocale) {
      return null;
    }

    let countryData = this._parse(this._addressData[`data/${country}`]);
    let locales = [];
    // TODO: Should be able to support multi-locale level 1/ level 2 metadata query
    //      in Bug 1421886
    if (countryData.languages) {
      let list = countryData.languages.filter(key => key !== countryData.lang);
      locales = list.map(key => this._parse(this._addressData[`${defaultLocale.id}--${key}`]));
    }
    return {defaultLocale, locales};
  },
};

this.FormAutofillUtils = {
  get AUTOFILL_FIELDS_THRESHOLD() { return 3; },

  ADDRESSES_COLLECTION_NAME,
  CREDITCARDS_COLLECTION_NAME,
  MANAGE_ADDRESSES_KEYWORDS,
  EDIT_ADDRESS_KEYWORDS,
  MANAGE_CREDITCARDS_KEYWORDS,
  EDIT_CREDITCARD_KEYWORDS,
  MAX_FIELD_VALUE_LENGTH,
  FIELD_STATES,
  SECTION_TYPES,

  _fieldNameInfo: {
    "name": "name",
    "given-name": "name",
    "additional-name": "name",
    "family-name": "name",
    "organization": "organization",
    "street-address": "address",
    "address-line1": "address",
    "address-line2": "address",
    "address-line3": "address",
    "address-level1": "address",
    "address-level2": "address",
    "postal-code": "address",
    "country": "address",
    "country-name": "address",
    "tel": "tel",
    "tel-country-code": "tel",
    "tel-national": "tel",
    "tel-area-code": "tel",
    "tel-local": "tel",
    "tel-local-prefix": "tel",
    "tel-local-suffix": "tel",
    "tel-extension": "tel",
    "email": "email",
    "cc-name": "creditCard",
    "cc-given-name": "creditCard",
    "cc-additional-name": "creditCard",
    "cc-family-name": "creditCard",
    "cc-number": "creditCard",
    "cc-exp-month": "creditCard",
    "cc-exp-year": "creditCard",
    "cc-exp": "creditCard",
    "cc-type": "creditCard",
  },

  _collators: {},
  _reAlternativeCountryNames: {},

  isAddressField(fieldName) {
    return !!this._fieldNameInfo[fieldName] && !this.isCreditCardField(fieldName);
  },

  isCreditCardField(fieldName) {
    return this._fieldNameInfo[fieldName] == "creditCard";
  },

  isCCNumber(ccNumber) {
    return CreditCard.isValidNumber(ccNumber);
  },

  /**
   * Get the array of credit card network ids ("types") we expect and offer as valid choices
   *
   * @returns {Array}
   */
  getCreditCardNetworks() {
    return CreditCard.SUPPORTED_NETWORKS;
  },

  getCategoryFromFieldName(fieldName) {
    return this._fieldNameInfo[fieldName];
  },

  getCategoriesFromFieldNames(fieldNames) {
    let categories = new Set();
    for (let fieldName of fieldNames) {
      let info = this.getCategoryFromFieldName(fieldName);
      if (info) {
        categories.add(info);
      }
    }
    return Array.from(categories);
  },

  getAddressSeparator() {
    // The separator should be based on the L10N address format, and using a
    // white space is a temporary solution.
    return " ";
  },

  /**
   * Get address display label. It should display information separated
   * by a comma.
   *
   * @param  {object} address
   * @param  {string?} addressFields Override the fields which can be displayed, but not the order.
   * @returns {string}
   */
  getAddressLabel(address, addressFields = null) {
    // TODO: Implement a smarter way for deciding what to display
    //       as option text. Possibly improve the algorithm in
    //       ProfileAutoCompleteResult.jsm and reuse it here.
    let fieldOrder = [
      "name",
      "-moz-street-address-one-line",  // Street address
      "address-level3",  // Townland / Neighborhood / Village
      "address-level2",  // City/Town
      "organization",    // Company or organization name
      "address-level1",  // Province/State (Standardized code if possible)
      "country-name",    // Country name
      "postal-code",     // Postal code
      "tel",             // Phone number
      "email",           // Email address
    ];

    address = {...address};
    let parts = [];
    if (addressFields) {
      let requiredFields = addressFields.trim().split(/\s+/);
      fieldOrder = fieldOrder.filter(name => requiredFields.includes(name));
    }
    if (address["street-address"]) {
      address["-moz-street-address-one-line"] = this.toOneLineAddress(
        address["street-address"]
      );
    }
    for (const fieldName of fieldOrder) {
      let string = address[fieldName];
      if (string) {
        parts.push(string);
      }
      if (parts.length == 2 && !addressFields) {
        break;
      }
    }
    return parts.join(", ");
  },

  toOneLineAddress(address, delimiter = "\n") {
    let array = typeof address == "string" ? address.split(delimiter) : address;

    if (!Array.isArray(array)) {
      return "";
    }
    return array
      .map(s => s ? s.trim() : "")
      .filter(s => s)
      .join(this.getAddressSeparator());
  },

  /**
   * In-place concatenate tel-related components into a single "tel" field and
   * delete unnecessary fields.
   * @param {object} address An address record.
   */
  compressTel(address) {
    let telCountryCode = address["tel-country-code"] || "";
    let telAreaCode = address["tel-area-code"] || "";

    if (!address.tel) {
      if (address["tel-national"]) {
        address.tel = telCountryCode + address["tel-national"];
      } else if (address["tel-local"]) {
        address.tel = telCountryCode + telAreaCode + address["tel-local"];
      } else if (address["tel-local-prefix"] && address["tel-local-suffix"]) {
        address.tel = telCountryCode + telAreaCode + address["tel-local-prefix"] + address["tel-local-suffix"];
      }
    }

    for (let field in address) {
      if (field != "tel" && this.getCategoryFromFieldName(field) == "tel") {
        delete address[field];
      }
    }
  },

  autofillFieldSelector(doc) {
    return doc.querySelectorAll("input, select");
  },

  ALLOWED_TYPES: ["text", "email", "tel", "number", "month"],
  isFieldEligibleForAutofill(element) {
    let tagName = element.tagName;
    if (tagName == "INPUT") {
      // `element.type` can be recognized as `text`, if it's missing or invalid.
      if (!this.ALLOWED_TYPES.includes(element.type)) {
        return false;
      }
    } else if (tagName != "SELECT") {
      return false;
    }

    return true;
  },

  loadDataFromScript(url, sandbox = {}) {
    Services.scriptloader.loadSubScript(url, sandbox, "utf-8");
    return sandbox;
  },

  /**
   * Get country address data and fallback to US if not found.
   * See AddressDataLoader._loadData for more details of addressData structure.
   * @param {string} [country=FormAutofill.DEFAULT_REGION]
   *        The country code for requesting specific country's metadata. It'll be
   *        default region if parameter is not set.
   * @param {string} [level1=null]
   *        Return address level 1/level 2 metadata if parameter is set.
   * @returns {object|null}
   *          Return metadata of specific region with default locale and other supported
   *          locales. We need to return a default country metadata for layout format
   *          and collator, but for sub-region metadata we'll just return null if not found.
   */
  getCountryAddressRawData(country = FormAutofill.DEFAULT_REGION, level1 = null) {
    let metadata = AddressDataLoader.getData(country, level1);
    if (!metadata) {
      if (level1) {
        return null;
      }
      // Fallback to default region if we couldn't get data from given country.
      if (country != FormAutofill.DEFAULT_REGION) {
        metadata = AddressDataLoader.getData(FormAutofill.DEFAULT_REGION);
      }
    }

    // TODO: Now we fallback to US if we couldn't get data from default region,
    //       but it could be removed in bug 1423464 if it's not necessary.
    if (!metadata) {
      metadata = AddressDataLoader.getData("US");
    }
    return metadata;
  },

  /**
   * Get country address data with default locale.
   * @param {string} country
   * @param {string} level1
   * @returns {object|null} Return metadata of specific region with default locale.
   *          NOTE: The returned data may be for a default region if the
   *          specified one cannot be found. Callers who only want the specific
   *          region should check the returned country code.
   */
  getCountryAddressData(country, level1) {
    let metadata = this.getCountryAddressRawData(country, level1);
    return metadata && metadata.defaultLocale;
  },

  /**
   * Get country address data with all locales.
   * @param {string} country
   * @param {string} level1
   * @returns {array<object>|null}
   *          Return metadata of specific region with all the locales.
   *          NOTE: The returned data may be for a default region if the
   *          specified one cannot be found. Callers who only want the specific
   *          region should check the returned country code.
   */
  getCountryAddressDataWithLocales(country, level1) {
    let metadata = this.getCountryAddressRawData(country, level1);
    return metadata && [metadata.defaultLocale, ...metadata.locales];
  },

  /**
   * Get the collators based on the specified country.
   * @param   {string} country The specified country.
   * @returns {array} An array containing several collator objects.
   */
  getCollators(country) {
    // TODO: Only one language should be used at a time per country. The locale
    //       of the page should be taken into account to do this properly.
    //       We are going to support more countries in bug 1370193 and this
    //       should be addressed when we start to implement that bug.

    if (!this._collators[country]) {
      let dataset = this.getCountryAddressData(country);
      let languages = dataset.languages || [dataset.lang];
      this._collators[country] = languages.map(lang => new Intl.Collator(lang, {sensitivity: "base", ignorePunctuation: true}));
    }
    return this._collators[country];
  },

  // Based on the list of fields abbreviations in
  // https://github.com/googlei18n/libaddressinput/wiki/AddressValidationMetadata
  FIELDS_LOOKUP: {
    N: "name",
    O: "organization",
    A: "street-address",
    S: "address-level1",
    C: "address-level2",
    D: "address-level3",
    Z: "postal-code",
    n: "newLine",
  },

  /**
   * Parse a country address format string and outputs an array of fields.
   * Spaces, commas, and other literals are ignored in this implementation.
   * For example, format string "%A%n%C, %S" should return:
   * [
   *   {fieldId: "street-address", newLine: true},
   *   {fieldId: "address-level2"},
   *   {fieldId: "address-level1"},
   * ]
   *
   * @param   {string} fmt Country address format string
   * @returns {array<object>} List of fields
   */
  parseAddressFormat(fmt) {
    if (!fmt) {
      throw new Error("fmt string is missing.");
    }

    return fmt.match(/%[^%]/g).reduce((parsed, part) => {
      // Take the first letter of each segment and try to identify it
      let fieldId = this.FIELDS_LOOKUP[part[1]];
      // Early return if cannot identify part.
      if (!fieldId) {
        return parsed;
      }
      // If a new line is detected, add an attribute to the previous field.
      if (fieldId == "newLine") {
        let size = parsed.length;
        if (size) {
          parsed[size - 1].newLine = true;
        }
        return parsed;
      }
      return parsed.concat({fieldId});
    }, []);
  },

  /**
   * Parse a require string and outputs an array of fields.
   * Spaces, commas, and other literals are ignored in this implementation.
   * For example, a require string "ACS" should return:
   * ["street-address", "address-level2", "address-level1"]
   *
   * @param   {string} requireString Country address require string
   * @returns {array<string>} List of fields
   */
  parseRequireString(requireString) {
    if (!requireString) {
      throw new Error("requireString string is missing.");
    }

    return requireString.split("").map(fieldId => this.FIELDS_LOOKUP[fieldId]);
  },

  /**
   * Use alternative country name list to identify a country code from a
   * specified country name.
   * @param   {string} countryName A country name to be identified
   * @param   {string} [countrySpecified] A country code indicating that we only
   *                                      search its alternative names if specified.
   * @returns {string} The matching country code.
   */
  identifyCountryCode(countryName, countrySpecified) {
    let countries = countrySpecified ? [countrySpecified] : [...FormAutofill.countries.keys()];

    for (let country of countries) {
      let collators = this.getCollators(country);
      let metadata = this.getCountryAddressData(country);
      if (country != metadata.key) {
        // We hit the fallback logic in getCountryAddressRawData so ignore it as
        // it's not related to `country` and use the name from l10n instead.
        metadata = {
          id: `data/${country}`,
          key: country,
          name: FormAutofill.countries.get(country),
        };
      }
      let alternativeCountryNames = metadata.alternative_names || [metadata.name];
      let reAlternativeCountryNames = this._reAlternativeCountryNames[country];
      if (!reAlternativeCountryNames) {
        reAlternativeCountryNames = this._reAlternativeCountryNames[country] = [];
      }

      for (let i = 0; i < alternativeCountryNames.length; i++) {
        let name = alternativeCountryNames[i];
        let reName = reAlternativeCountryNames[i];
        if (!reName) {
          reName = reAlternativeCountryNames[i] = new RegExp("\\b" + this.escapeRegExp(name) + "\\b", "i");
        }

        if (this.strCompare(name, countryName, collators) || reName.test(countryName)) {
          return country;
        }
      }
    }

    return null;
  },

  findSelectOption(selectEl, record, fieldName) {
    if (this.isAddressField(fieldName)) {
      return this.findAddressSelectOption(selectEl, record, fieldName);
    }
    if (this.isCreditCardField(fieldName)) {
      return this.findCreditCardSelectOption(selectEl, record, fieldName);
    }
    return null;
  },

  /**
   * Try to find the abbreviation of the given sub-region name
   * @param   {string[]} subregionValues A list of inferable sub-region values.
   * @param   {string} [country] A country name to be identified.
   * @returns {string} The matching sub-region abbreviation.
   */
  getAbbreviatedSubregionName(subregionValues, country) {
    let values = Array.isArray(subregionValues) ? subregionValues : [subregionValues];

    let collators = this.getCollators(country);
    for (let metadata of this.getCountryAddressDataWithLocales(country)) {
      let {sub_keys: subKeys, sub_names: subNames, sub_lnames: subLnames} = metadata;
      if (!subKeys) {
        // Not all regions have sub_keys. e.g. DE
        continue;
      }
      // Apply sub_lnames if sub_names does not exist
      subNames = subNames || subLnames;

      let speculatedSubIndexes = [];
      for (const val of values) {
        let identifiedValue = this.identifyValue(subKeys, subNames, val, collators);
        if (identifiedValue) {
          return identifiedValue;
        }

        // Predict the possible state by partial-matching if no exact match.
        [subKeys, subNames].forEach(sub => {
          speculatedSubIndexes.push(sub.findIndex(token => {
            let pattern = new RegExp("\\b" + this.escapeRegExp(token) + "\\b");

            return pattern.test(val);
          }));
        });
      }
      let subKey = subKeys[speculatedSubIndexes.find(i => !!~i)];
      if (subKey) {
        return subKey;
      }
    }
    return null;
  },

  /**
   * Find the option element from select element.
   * 1. Try to find the locale using the country from address.
   * 2. First pass try to find exact match.
   * 3. Second pass try to identify values from address value and options,
   *    and look for a match.
   * @param   {DOMElement} selectEl
   * @param   {object} address
   * @param   {string} fieldName
   * @returns {DOMElement}
   */
  findAddressSelectOption(selectEl, address, fieldName) {
    let value = address[fieldName];
    if (!value) {
      return null;
    }

    let collators = this.getCollators(address.country);

    for (let option of selectEl.options) {
      if (this.strCompare(value, option.value, collators) ||
          this.strCompare(value, option.text, collators)) {
        return option;
      }
    }

    switch (fieldName) {
      case "address-level1": {
        let {country} = address;
        let identifiedValue = this.getAbbreviatedSubregionName([value], country);
        // No point going any further if we cannot identify value from address level 1
        if (!identifiedValue) {
          return null;
        }
        for (let dataset of this.getCountryAddressDataWithLocales(country)) {
          let keys = dataset.sub_keys;
          if (!keys) {
            // Not all regions have sub_keys. e.g. DE
            continue;
          }
          // Apply sub_lnames if sub_names does not exist
          let names = dataset.sub_names || dataset.sub_lnames;

          // 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" + this.escapeRegExp(identifiedValue) + "\\b", "i");
          for (let option of selectEl.options) {
            let optionValue = this.identifyValue(keys, names, option.value, collators);
            let optionText = this.identifyValue(keys, names, option.text, collators);
            if (identifiedValue === optionValue || identifiedValue === optionText || pattern.test(option.value)) {
              return option;
            }
          }
        }
        break;
      }
      case "country": {
        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;
            }
          }
        }
        break;
      }
    }

    return null;
  },

  findCreditCardSelectOption(selectEl, creditCard, fieldName) {
    let oneDigitMonth = creditCard["cc-exp-month"] ? creditCard["cc-exp-month"].toString() : null;
    let twoDigitsMonth = oneDigitMonth ? oneDigitMonth.padStart(2, "0") : null;
    let fourDigitsYear = creditCard["cc-exp-year"] ? creditCard["cc-exp-year"].toString() : null;
    let twoDigitsYear = fourDigitsYear ? fourDigitsYear.substr(2, 2) : null;
    let options = Array.from(selectEl.options);

    switch (fieldName) {
      case "cc-exp-month": {
        if (!oneDigitMonth) {
          return null;
        }
        for (let option of options) {
          if ([option.text, option.label, option.value].some(s => {
            let result = /[1-9]\d*/.exec(s);
            return result && result[0] == oneDigitMonth;
          })) {
            return option;
          }
        }
        break;
      }
      case "cc-exp-year": {
        if (!fourDigitsYear) {
          return null;
        }
        for (let option of options) {
          if ([option.text, option.label, option.value].some(
            s => s == twoDigitsYear || s == fourDigitsYear
          )) {
            return option;
          }
        }
        break;
      }
      case "cc-exp": {
        if (!oneDigitMonth || !fourDigitsYear) {
          return null;
        }
        let patterns = [
          oneDigitMonth + "/" + twoDigitsYear,    // 8/22
          oneDigitMonth + "/" + fourDigitsYear,   // 8/2022
          twoDigitsMonth + "/" + twoDigitsYear,   // 08/22
          twoDigitsMonth + "/" + fourDigitsYear,  // 08/2022
          oneDigitMonth + "-" + twoDigitsYear,    // 8-22
          oneDigitMonth + "-" + fourDigitsYear,   // 8-2022
          twoDigitsMonth + "-" + twoDigitsYear,   // 08-22
          twoDigitsMonth + "-" + fourDigitsYear,  // 08-2022
          twoDigitsYear + "-" + twoDigitsMonth,   // 22-08
          fourDigitsYear + "-" + twoDigitsMonth,  // 2022-08
          fourDigitsYear + "/" + oneDigitMonth,   // 2022/8
          twoDigitsMonth + twoDigitsYear,         // 0822
          twoDigitsYear + twoDigitsMonth,         // 2208
        ];

        for (let option of options) {
          if ([option.text, option.label, option.value].some(
            str => patterns.some(pattern => str.includes(pattern))
          )) {
            return option;
          }
        }
        break;
      }
      case "cc-type": {
        let network = creditCard["cc-type"] || "";
        for (let option of options) {
          if ([option.text, option.label, option.value].some(
            s => s.trim().toLowerCase() == network
          )) {
            return option;
          }
        }
        break;
      }
    }

    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   {array} collators
   * @returns {string}
   */
  identifyValue(keys, names, value, collators) {
    let resultKey = keys.find(key => this.strCompare(value, key, collators));
    if (resultKey) {
      return resultKey;
    }

    let index = names.findIndex(name => this.strCompare(value, name, collators));
    if (index !== -1) {
      return keys[index];
    }

    return null;
  },

  /**
   * Compare if two strings are the same.
   * @param   {string} a
   * @param   {string} b
   * @param   {array} collators
   * @returns {boolean}
   */
  strCompare(a = "", b = "", collators) {
    return collators.some(collator => !collator.compare(a, b));
  },

  /**
   * Escaping user input to be treated as a literal string within a regular
   * expression.
   * @param   {string} string
   * @returns {string}
   */
  escapeRegExp(string) {
    return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  },

  /**
   * Get formatting information of a given country
   * @param   {string} country
   * @returns {object}
   *         {
   *           {string} addressLevel3Label
   *           {string} addressLevel2Label
   *           {string} addressLevel1Label
   *           {string} postalCodeLabel
   *           {object} fieldsOrder
   *           {string} postalCodePattern
   *         }
   */
  getFormFormat(country) {
    const dataset = this.getCountryAddressData(country);
    return {
      // When particular values are missing for a country, the
      // data/ZZ value should be used instead.
      addressLevel3Label: dataset.sublocality_name_type || "suburb",
      addressLevel2Label: dataset.locality_name_type || "city",
      addressLevel1Label: dataset.state_name_type || "province",
      postalCodeLabel: dataset.zip_name_type || "postalCode",
      fieldsOrder: this.parseAddressFormat(dataset.fmt || "%N%n%O%n%A%n%C"),
      postalCodePattern: dataset.zip,
      countryRequiredFields: this.parseRequireString(dataset.require || "AC"),
    };
  },

  /**
   * Localize "data-localization" or "data-localization-region" attributes.
   * @param {Element} element
   * @param {string} attributeName
   */
  localizeAttributeForElement(element, attributeName) {
    switch (attributeName) {
      case "data-localization": {
        element.textContent =
          this.stringBundle.GetStringFromName(element.getAttribute(attributeName));
        element.removeAttribute(attributeName);
        break;
      }
      case "data-localization-region": {
        let regionCode = element.getAttribute(attributeName);
        element.textContent = Services.intl.getRegionDisplayNames(undefined, [regionCode]);
        element.removeAttribute(attributeName);
        return;
      }
      default:
        throw new Error("Unexpected attributeName");
    }
  },

  /**
   * Localize elements with "data-localization" or "data-localization-region" attributes.
   * @param {Element} root
   */
  localizeMarkup(root) {
    let elements = root.querySelectorAll("[data-localization]");
    for (let element of elements) {
      this.localizeAttributeForElement(element, "data-localization");
    }

    elements = root.querySelectorAll("[data-localization-region]");
    for (let element of elements) {
      this.localizeAttributeForElement(element, "data-localization-region");
    }
  },
};

this.log = null;
this.FormAutofill.defineLazyLogGetter(this, EXPORTED_SYMBOLS[0]);

XPCOMUtils.defineLazyGetter(FormAutofillUtils, "stringBundle", function() {
  return Services.strings.createBundle("chrome://formautofill/locale/formautofill.properties");
});

XPCOMUtils.defineLazyGetter(FormAutofillUtils, "brandBundle", function() {
  return Services.strings.createBundle("chrome://branding/locale/brand.properties");
});
PK
!<ŽZݽz'z'chrome/res/OSKeyStore.jsm/* 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/. */

/**
 * Helpers for using OS Key Store.
 */

"use strict";

var EXPORTED_SYMBOLS = [
  "OSKeyStore",
];

ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");

ChromeUtils.defineModuleGetter(this, "AppConstants", "resource://gre/modules/AppConstants.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "nativeOSKeyStore",
                                   "@mozilla.org/security/oskeystore;1", Ci.nsIOSKeyStore);
XPCOMUtils.defineLazyServiceGetter(this, "osReauthenticator",
                                   "@mozilla.org/security/osreauthenticator;1", Ci.nsIOSReauthenticator);

// Skip reauth during tests, only works in non-official builds.
const TEST_ONLY_REAUTH = "extensions.formautofill.osKeyStore.unofficialBuildOnlyLogin";

var OSKeyStore = {
  /**
   * On macOS this becomes part of the name label visible on Keychain Acesss as
   * "org.mozilla.nss.keystore.firefox" (where "firefox" is the MOZ_APP_NAME).
   */
  STORE_LABEL: AppConstants.MOZ_APP_NAME,

  /**
   * Consider the module is initialized as locked. OS might unlock without a
   * prompt.
   * @type {Boolean}
   */
  _isLocked: true,

  _pendingUnlockPromise: null,

  /**
   * @returns {boolean} True if logged in (i.e. decrypt(reauth = false) will
   *                    not retrigger a dialog) and false if not.
   *                    User might log out elsewhere in the OS, so even if this
   *                    is true a prompt might still pop up.
   */
  get isLoggedIn() {
    return !this._isLocked;
  },

  /**
   * @returns {boolean} True if there is another login dialog existing and false
   *                    otherwise.
   */
  get isUIBusy() {
    return !!this._pendingUnlockPromise;
  },

  /**
   * If the test pref exists, this method will dispatch a observer message and
   * resolves to simulate successful reauth, or rejects to simulate failed reauth.
   *
   * @returns {Promise<undefined>} Resolves when sucessful login, rejects when
   *                               login fails.
   */
  async _reauthInTests() {
    // Skip this reauth because there is no way to mock the
    // native dialog in the testing environment, for now.
    log.debug("_ensureReauth: _testReauth: ", this._testReauth);
    switch (this._testReauth) {
      case "pass":
        Services.obs.notifyObservers(null, "oskeystore-testonly-reauth", "pass");
        break;
      case "cancel":
        Services.obs.notifyObservers(null, "oskeystore-testonly-reauth", "cancel");
        throw new Components.Exception("Simulating user cancelling login dialog", Cr.NS_ERROR_FAILURE);
      default:
        throw new Components.Exception("Unknown test pref value", Cr.NS_ERROR_FAILURE);
    }
  },

  /**
   * Ensure the store in use is logged in. It will display the OS login
   * login prompt or do nothing if it's logged in already. If an existing login
   * prompt is already prompted, the result from it will be used instead.
   *
   * Note: This method must set _pendingUnlockPromise before returning the
   * promise (i.e. the first |await|), otherwise we'll risk re-entry.
   * This is why there aren't an |await| in the method. The method is marked as
   * |async| to communicate that it's async.
   *
   * @param   {boolean|string} reauth If it's set to true or a string, prompt
   *                                  the reauth login dialog.
   *                                  The string will be shown on the native OS
   *                                  login dialog.
   * @returns {Promise<boolean>}      True if it's logged in or no password is set
   *                                  and false if it's still not logged in (prompt
   *                                  canceled or other error).
   */
  async ensureLoggedIn(reauth = false) {
    if (this._pendingUnlockPromise) {
      log.debug("ensureLoggedIn: Has a pending unlock operation");
      return this._pendingUnlockPromise;
    }
    log.debug("ensureLoggedIn: Creating new pending unlock promise. reauth: ", reauth);

    let unlockPromise;

    // Decides who should handle reauth
    if (!this._reauthEnabledByUser || (typeof reauth == "boolean" && !reauth)) {
      unlockPromise = Promise.resolve();
    } else if (!AppConstants.MOZILLA_OFFICIAL && this._testReauth) {
      unlockPromise = this._reauthInTests();
    } else if (AppConstants.platform == "win" ||
               AppConstants.platform == "macosx") {
      let reauthLabel = typeof reauth == "string" ? reauth : "";
      // On Windows, this promise rejects when the user cancels login dialog, see bug 1502121.
      // On macOS this resolves to false, so we would need to check it.
      unlockPromise = osReauthenticator.asyncReauthenticateUser(reauthLabel)
        .then(reauthResult => {
          if (typeof reauthResult == "boolean" && !reauthResult) {
            throw new Components.Exception("User canceled OS reauth entry", Cr.NS_ERROR_FAILURE);
          }
        });
    } else {
      log.debug("ensureLoggedIn: Skipping reauth on unsupported platforms");
      unlockPromise = Promise.resolve();
    }

    unlockPromise = unlockPromise.then(async () => {
      if (!await nativeOSKeyStore.asyncSecretAvailable(this.STORE_LABEL)) {
        log.debug("ensureLoggedIn: Secret unavailable, attempt to generate new secret.");
        let recoveryPhrase = await nativeOSKeyStore.asyncGenerateSecret(this.STORE_LABEL);
        // TODO We should somehow have a dialog to ask the user to write this down,
        // and another dialog somewhere for the user to restore the secret with it.
        // (Intentionally not printing it out in the console)
        log.debug("ensureLoggedIn: Secret generated. Recovery phrase length: " + recoveryPhrase.length);
      }
    });

    if (nativeOSKeyStore.isNSSKeyStore) {
      // Workaround bug 1492305: NSS-implemented methods don't reject when user cancels.
      unlockPromise = unlockPromise.then(() => {
        log.debug("ensureLoggedIn: isNSSKeyStore: ", reauth, Services.logins.isLoggedIn);
        // User has hit the cancel button on the master password prompt.
        // We must reject the promise chain here.
        if (!Services.logins.isLoggedIn) {
          throw Components.Exception("User canceled OS unlock entry (Workaround)", Cr.NS_ERROR_FAILURE);
        }
      });
    }

    unlockPromise = unlockPromise.then(() => {
      log.debug("ensureLoggedIn: Logged in");
      this._pendingUnlockPromise = null;
      this._isLocked = false;

      return true;
    }, (err) => {
      log.debug("ensureLoggedIn: Not logged in", err);
      this._pendingUnlockPromise = null;
      this._isLocked = true;

      return false;
    });

    this._pendingUnlockPromise = unlockPromise;

    return this._pendingUnlockPromise;
  },

  /**
   * Decrypts cipherText.
   *
   * Note: In the event of an rejection, check the result property of the Exception
   *       object. Handles NS_ERROR_ABORT as user has cancelled the action (e.g.,
   *       don't show that dialog), apart from other errors (e.g., gracefully
   *       recover from that and still shows the dialog.)
   *
   * @param   {string}         cipherText Encrypted string including the algorithm details.
   * @param   {boolean|string} reauth     If it's set to true or a string, prompt
   *                                      the reauth login dialog.
   *                                      The string may be shown on the native OS
   *                                      login dialog.
   * @returns {Promise<string>}           resolves to the decrypted string, or rejects otherwise.
   */
  async decrypt(cipherText, reauth = false) {
    if (!await this.ensureLoggedIn(reauth)) {
      throw Components.Exception("User canceled OS unlock entry", Cr.NS_ERROR_ABORT);
    }
    let bytes = await nativeOSKeyStore.asyncDecryptBytes(this.STORE_LABEL, cipherText);
    return String.fromCharCode.apply(String, bytes);
  },

  /**
   * Encrypts a string and returns cipher text containing algorithm information used for decryption.
   *
   * @param   {string} plainText Original string without encryption.
   * @returns {Promise<string>} resolves to the encrypted string (with algorithm), otherwise rejects.
   */
  async encrypt(plainText) {
    if (!await this.ensureLoggedIn()) {
      throw Components.Exception("User canceled OS unlock entry", Cr.NS_ERROR_ABORT);
    }

    // Convert plain text into a UTF-8 binary string
    plainText = unescape(encodeURIComponent(plainText));

    // Convert it to an array
    let textArr = [];
    for (let char of plainText) {
      textArr.push(char.charCodeAt(0));
    }

    let rawEncryptedText = await nativeOSKeyStore.asyncEncryptBytes(this.STORE_LABEL, textArr.length, textArr);

    // Mark the output with a version number.
    return rawEncryptedText;
  },

  /**
   * Resolve when the login dialogs are closed, immediately if none are open.
   *
   * An existing MP dialog will be focused and will request attention.
   *
   * @returns {Promise<boolean>}
   *          Resolves with whether the user is logged in to MP.
   */
  async waitForExistingDialog() {
    if (this.isUIBusy) {
      return this._pendingUnlockPromise;
    }
    return this.isLoggedIn;
  },

  /**
   * Remove the store. For tests.
   */
  async cleanup() {
    return nativeOSKeyStore.asyncDeleteSecret(this.STORE_LABEL);
  },
};

XPCOMUtils.defineLazyGetter(this, "log", () => {
  let ConsoleAPI = ChromeUtils.import("resource://gre/modules/Console.jsm", {}).ConsoleAPI;
  return new ConsoleAPI({
    maxLogLevelPref: "extensions.formautofill.loglevel",
    prefix: "OSKeyStore",
  });
});

XPCOMUtils.defineLazyPreferenceGetter(OSKeyStore, "_testReauth", TEST_ONLY_REAUTH, "");
XPCOMUtils.defineLazyPreferenceGetter(OSKeyStore, "_reauthEnabledByUser",
                                      "extensions.formautofill.reauth.enabled", false);
PK
!<±֙ñ1ñ1(chrome/res/ProfileAutoCompleteResult.jsm/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

var EXPORTED_SYMBOLS = ["AddressResult", "CreditCardResult"];

ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.import("resource://formautofill/FormAutofill.jsm");
ChromeUtils.defineModuleGetter(this, "FormAutofillUtils",
                               "resource://formautofill/FormAutofillUtils.jsm");
ChromeUtils.defineModuleGetter(this, "CreditCard",
  "resource://gre/modules/CreditCard.jsm");

XPCOMUtils.defineLazyPreferenceGetter(this, "insecureWarningEnabled", "security.insecure_field_warning.contextual.enabled");

this.log = null;
FormAutofill.defineLazyLogGetter(this, EXPORTED_SYMBOLS[0]);

class ProfileAutoCompleteResult {
  constructor(searchString, focusedFieldName, allFieldNames, matchingProfiles, {
    resultCode = null,
    isSecure = true,
    isInputAutofilled = false,
  }) {
    log.debug("Constructing new ProfileAutoCompleteResult:", [...arguments]);

    // nsISupports
    this.QueryInterface = ChromeUtils.generateQI([Ci.nsIAutoCompleteResult]);

    // The user's query string
    this.searchString = searchString;
    // The field name of the focused input.
    this._focusedFieldName = focusedFieldName;
    // The matching profiles contains the information for filling forms.
    this._matchingProfiles = matchingProfiles;
    // The default item that should be entered if none is selected
    this.defaultIndex = 0;
    // The reason the search failed
    this.errorDescription = "";
    // The value used to determine whether the form is secure or not.
    this._isSecure = isSecure;
    // The value to indicate whether the focused input has been autofilled or not.
    this._isInputAutofilled = isInputAutofilled;
    // All fillable field names in the form including the field name of the currently-focused input.
    this._allFieldNames = [...this._matchingProfiles.reduce((fieldSet, curProfile) => {
      for (let field of Object.keys(curProfile)) {
        fieldSet.add(field);
      }

      return fieldSet;
    }, new Set())].filter(field => allFieldNames.includes(field));

    // Force return success code if the focused field is auto-filled in order
    // to show clear form button popup.
    if (isInputAutofilled) {
      resultCode = Ci.nsIAutoCompleteResult.RESULT_SUCCESS;
    }
    // The result code of this result object.
    if (resultCode) {
      this.searchResult = resultCode;
    } else if (matchingProfiles.length > 0) {
      this.searchResult = Ci.nsIAutoCompleteResult.RESULT_SUCCESS;
    } else {
      this.searchResult = Ci.nsIAutoCompleteResult.RESULT_NOMATCH;
    }

    // An array of primary and secondary labels for each profile.
    this._popupLabels = this._generateLabels(this._focusedFieldName,
                                             this._allFieldNames,
                                             this._matchingProfiles);
  }

  /**
   * @returns {number} The number of results
   */
  get matchCount() {
    return this._popupLabels.length;
  }

  _checkIndexBounds(index) {
    if (index < 0 || index >= this._popupLabels.length) {
      throw Components.Exception("Index out of range.", Cr.NS_ERROR_ILLEGAL_VALUE);
    }
  }

  /**
   * Get the secondary label based on the focused field name and related field names
   * in the same form.
   * @param   {string} focusedFieldName The field name of the focused input
   * @param   {Array<Object>} allFieldNames The field names in the same section
   * @param   {object} profile The profile providing the labels to show.
   * @returns {string} The secondary label
   */
  _getSecondaryLabel(focusedFieldName, allFieldNames, profile) {
    return "";
  }

  _generateLabels(focusedFieldName, allFieldNames, profiles) {}

  /**
   * Get the value of the result at the given index.
   *
   * Always return empty string for form autofill feature to suppress
   * AutoCompleteController from autofilling, as we'll populate the
   * fields on our own.
   *
   * @param   {number} index The index of the result requested
   * @returns {string} The result at the specified index
   */
  getValueAt(index) {
    this._checkIndexBounds(index);
    return "";
  }

  getLabelAt(index) {
    this._checkIndexBounds(index);

    let label = this._popupLabels[index];
    if (typeof label == "string") {
      return label;
    }
    return JSON.stringify(label);
  }

  /**
   * Retrieves a comment (metadata instance)
   * @param   {number} index The index of the comment requested
   * @returns {string} The comment at the specified index
   */
  getCommentAt(index) {
    this._checkIndexBounds(index);
    return JSON.stringify(this._matchingProfiles[index]);
  }

  /**
   * Retrieves a style hint specific to a particular index.
   * @param   {number} index The index of the style hint requested
   * @returns {string} The style hint at the specified index
   */
  getStyleAt(index) {
    this._checkIndexBounds(index);
    if (index == this.matchCount - 1) {
      return "autofill-footer";
    }
    if (this._isInputAutofilled) {
      return "autofill-clear-button";
    }

    return "autofill-profile";
  }

  /**
   * Retrieves an image url.
   * @param   {number} index The index of the image url requested
   * @returns {string} The image url at the specified index
   */
  getImageAt(index) {
    this._checkIndexBounds(index);
    return "";
  }

  /**
   * Retrieves a result
   * @param   {number} index The index of the result requested
   * @returns {string} The result at the specified index
   */
  getFinalCompleteValueAt(index) {
    return this.getValueAt(index);
  }

  /**
   * Removes a result from the resultset
   * @param {number} index The index of the result to remove
   * @param {boolean} removeFromDatabase TRUE for removing data from DataBase
   *                                     as well.
   */
  removeValueAt(index, removeFromDatabase) {
    // There is no plan to support removing profiles via autocomplete.
  }
}

class AddressResult extends ProfileAutoCompleteResult {
  constructor(...args) {
    super(...args);
  }

  _getSecondaryLabel(focusedFieldName, allFieldNames, profile) {
    // We group similar fields into the same field name so we won't pick another
    // field in the same group as the secondary label.
    const GROUP_FIELDS = {
      "name": [
        "name",
        "given-name",
        "additional-name",
        "family-name",
      ],
      "street-address": [
        "street-address",
        "address-line1",
        "address-line2",
        "address-line3",
      ],
      "country-name": [
        "country",
        "country-name",
      ],
      "tel": [
        "tel",
        "tel-country-code",
        "tel-national",
        "tel-area-code",
        "tel-local",
        "tel-local-prefix",
        "tel-local-suffix",
      ],
    };

    const secondaryLabelOrder = [
      "street-address",  // Street address
      "name",            // Full name
      "address-level3",  // Townland / Neighborhood / Village
      "address-level2",  // City/Town
      "organization",    // Company or organization name
      "address-level1",  // Province/State (Standardized code if possible)
      "country-name",    // Country name
      "postal-code",     // Postal code
      "tel",             // Phone number
      "email",           // Email address
    ];

    for (let field in GROUP_FIELDS) {
      if (GROUP_FIELDS[field].includes(focusedFieldName)) {
        focusedFieldName = field;
        break;
      }
    }

    for (const currentFieldName of secondaryLabelOrder) {
      if (focusedFieldName == currentFieldName || !profile[currentFieldName]) {
        continue;
      }

      let matching = GROUP_FIELDS[currentFieldName] ?
        allFieldNames.some(fieldName => GROUP_FIELDS[currentFieldName].includes(fieldName)) :
        allFieldNames.includes(currentFieldName);

      if (matching) {
        if (currentFieldName == "street-address" &&
            profile["-moz-street-address-one-line"]) {
          return profile["-moz-street-address-one-line"];
        }
        return profile[currentFieldName];
      }
    }

    return ""; // Nothing matched.
  }

  _generateLabels(focusedFieldName, allFieldNames, profiles) {
    if (this._isInputAutofilled) {
      return [
        {primary: "", secondary: ""}, // Clear button
        {primary: "", secondary: ""}, // Footer
      ];
    }

    // Skip results without a primary label.
    let labels = profiles.filter(profile => {
      return !!profile[focusedFieldName];
    }).map(profile => {
      let primaryLabel = profile[focusedFieldName];
      if (focusedFieldName == "street-address" &&
          profile["-moz-street-address-one-line"]) {
        primaryLabel = profile["-moz-street-address-one-line"];
      }
      return {
        primary: primaryLabel,
        secondary: this._getSecondaryLabel(focusedFieldName,
                                           allFieldNames,
                                           profile),
      };
    });
    // Add an empty result entry for footer. Its content will come from
    // the footer binding, so don't assign any value to it.
    // The additional properties: categories and focusedCategory are required of
    // the popup to generate autofill hint on the footer.
    labels.push({
      primary: "",
      secondary: "",
      categories: FormAutofillUtils.getCategoriesFromFieldNames(this._allFieldNames),
      focusedCategory: FormAutofillUtils.getCategoryFromFieldName(this._focusedFieldName),
    });

    return labels;
  }
}

class CreditCardResult extends ProfileAutoCompleteResult {
  constructor(...args) {
    super(...args);
  }

  _getSecondaryLabel(focusedFieldName, allFieldNames, profile) {
    const GROUP_FIELDS = {
      "cc-name": [
        "cc-name",
        "cc-given-name",
        "cc-additional-name",
        "cc-family-name",
      ],
      "cc-exp": [
        "cc-exp",
        "cc-exp-month",
        "cc-exp-year",
      ],
    };

    const secondaryLabelOrder = [
      "cc-number",       // Credit card number
      "cc-name",         // Full name
      "cc-exp",          // Expiration date
    ];

    for (let field in GROUP_FIELDS) {
      if (GROUP_FIELDS[field].includes(focusedFieldName)) {
        focusedFieldName = field;
        break;
      }
    }

    for (const currentFieldName of secondaryLabelOrder) {
      if (focusedFieldName == currentFieldName || !profile[currentFieldName]) {
        continue;
      }

      let matching = GROUP_FIELDS[currentFieldName] ?
        allFieldNames.some(fieldName => GROUP_FIELDS[currentFieldName].includes(fieldName)) :
        allFieldNames.includes(currentFieldName);

      if (matching) {
        if (currentFieldName == "cc-number") {
          let {affix, label} = CreditCard.formatMaskedNumber(profile[currentFieldName]);
          return affix + label;
        }
        return profile[currentFieldName];
      }
    }

    return ""; // Nothing matched.
  }

  _generateLabels(focusedFieldName, allFieldNames, profiles) {
    if (!this._isSecure) {
      if (!insecureWarningEnabled) {
        return [];
      }
      let brandName = FormAutofillUtils.brandBundle.GetStringFromName("brandShortName");

      return [FormAutofillUtils.stringBundle.formatStringFromName("insecureFieldWarningDescription", [brandName], 1)];
    }

    if (this._isInputAutofilled) {
      return [
        {primary: "", secondary: ""}, // Clear button
        {primary: "", secondary: ""}, // Footer
      ];
    }

    // Skip results without a primary label.
    let labels = profiles.filter(profile => {
      return !!profile[focusedFieldName];
    }).map(profile => {
      let primaryAffix;
      let primary = profile[focusedFieldName];

      if (focusedFieldName == "cc-number") {
        let {affix, label} = CreditCard.formatMaskedNumber(primary);
        primaryAffix = affix;
        primary = label;
      }
      return {
        primaryAffix,
        primary,
        secondary: this._getSecondaryLabel(focusedFieldName,
                                           allFieldNames,
                                           profile),
      };
    });
    // Add an empty result entry for footer.
    labels.push({primary: "", secondary: ""});

    return labels;
  }

  getStyleAt(index) {
    this._checkIndexBounds(index);
    if (!this._isSecure && insecureWarningEnabled) {
      return "autofill-insecureWarning";
    }

    return super.getStyleAt(index);
  }

  getImageAt(index) {
    this._checkIndexBounds(index);
    return "chrome://formautofill/content/icon-credit-card-generic.svg";
  }
}
PK
!<	N~~/chrome/res/addressmetadata/addressReferences.js/* 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

// See https://github.com/googlei18n/libaddressinput/wiki/AddressValidationMetadata for
// documentation on how to use the data.

// 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/AD": {"fmt": "%N%n%O%n%A%n%Z %C", "id": "data/AD", "key": "AD", "lang": "ca", "languages": "ca", "name": "ANDORRA", "posturl": "http://www.correos.es/comun/CodigosPostales/1010_s-CodPostal.asp?Provincia=", "sub_isoids": "07~02~03~08~04~05~06", "sub_keys": "Parròquia d'Andorra la Vella~Canillo~Encamp~Escaldes-Engordany~La Massana~Ordino~Sant Julià de Lòria", "sub_names": "Andorra la Vella~Canillo~Encamp~Escaldes-Engordany~La Massana~Ordino~Sant Julià de Lòria", "sub_zipexs": "AD500~AD100~AD200~AD700~AD400~AD300~AD600", "sub_zips": "AD50[01]~AD10[01]~AD20[01]~AD70[01]~AD40[01]~AD30[01]~AD60[01]", "zip": "AD[1-7]0\\d", "zipex": "AD100,AD501,AD700"},
  "data/AE": {"fmt": "%N%n%O%n%A%n%S", "id": "data/AE", "key": "AE", "lang": "ar", "languages": "ar", "lfmt": "%N%n%O%n%A%n%S", "name": "UNITED ARAB EMIRATES", "require": "AS", "state_name_type": "emirate", "sub_isoids": "AZ~SH~FU~UQ~DU~RK~AJ", "sub_keys": "أبو ظبي~إمارة الشارقةّ~الفجيرة~ام القيوين~إمارة دبيّ~إمارة رأس الخيمة~عجمان", "sub_lnames": "Abu Dhabi~Sharjah~Fujairah~Umm Al Quwain~Dubai~Ras al Khaimah~Ajman", "sub_names": "أبو ظبي~الشارقة~الفجيرة~ام القيوين~دبي~رأس الخيمة~عجمان"},
  "data/AF": {"fmt": "%N%n%O%n%A%n%C%n%Z", "id": "data/AF", "key": "AF", "name": "AFGHANISTAN", "zip": "\\d{4}", "zipex": "1001,2601,3801"},
  "data/AG": {"id": "data/AG", "key": "AG", "name": "ANTIGUA AND BARBUDA", "require": "A"},
  "data/AI": {"fmt": "%N%n%O%n%A%n%C%n%Z", "id": "data/AI", "key": "AI", "name": "ANGUILLA", "zip": "(?:AI-)?2640", "zipex": "2640"},
  "data/AL": {"fmt": "%N%n%O%n%A%n%Z%n%C", "id": "data/AL", "key": "AL", "name": "ALBANIA", "zip": "\\d{4}", "zipex": "1001,1017,3501"},
  "data/AM": {"fmt": "%N%n%O%n%A%n%Z%n%C%n%S", "id": "data/AM", "key": "AM", "lang": "hy", "languages": "hy", "lfmt": "%N%n%O%n%A%n%Z%n%C%n%S", "name": "ARMENIA", "sub_isoids": "AG~AR~AV~GR~ER~LO~KT~SH~SU~VD~TV", "sub_keys": "Արագածոտն~Արարատ~Արմավիր~Գեղարքունիք~Երևան~Լոռի~Կոտայք~Շիրակ~Սյունիք~Վայոց ձոր~Տավուշ", "sub_lnames": "Aragatsotn~Ararat~Armavir~Gegharkunik~Yerevan~Lori~Kotayk~Shirak~Syunik~Vayots Dzor~Tavush", "sub_zipexs": "0201,0514~0601,0823~0901,1149~1201,1626~0000,0099~1701,2117~2201,2506~2601,3126~3201,3519~3601,3810~3901,4216", "sub_zips": "0[2-5]~0[6-8]~09|1[01]~1[2-6]~00~1[7-9]|2[01]~2[2-5]~2[6-9]|3[01]~3[2-5]~3[6-8]~39|4[0-2]", "zip": "(?:37)?\\d{4}", "zipex": "375010,0002,0010"},
  "data/AO": {"id": "data/AO", "key": "AO", "name": "ANGOLA"},
  "data/AQ": {"id": "data/AQ", "key": "AQ", "name": "ANTARCTICA"},
  "data/AR": {"fmt": "%N%n%O%n%A%n%Z %C%n%S", "id": "data/AR", "key": "AR", "lang": "es", "languages": "es", "name": "ARGENTINA", "posturl": "http://www.correoargentino.com.ar/formularios/cpa", "sub_isoids": "B~K~H~U~C~X~W~E~P~Y~L~F~M~N~Q~R~A~J~D~Z~S~G~V~T", "sub_keys": "Buenos Aires~Catamarca~Chaco~Chubut~Ciudad Autónoma de Buenos Aires~Córdoba~Corrientes~Entre Ríos~Formosa~Jujuy~La Pampa~La Rioja~Mendoza~Misiones~Neuquén~Río Negro~Salta~San Juan~San Luis~Santa Cruz~Santa Fe~Santiago del Estero~Tierra del Fuego~Tucumán", "sub_names": "Buenos Aires~Catamarca~Chaco~Chubut~Ciudad Autónoma de Buenos Aires~Córdoba~Corrientes~Entre Ríos~Formosa~Jujuy~La Pampa~La Rioja~Mendoza~Misiones~Neuquén~Río Negro~Salta~San Juan~San Luis~Santa Cruz~Santa Fe~Santiago del Estero~Tierra del Fuego~Tucumán", "sub_zips": "B?[1-36-8]~K?[45]~H?3~U?[89]~C?1~X?[235-8]~W?3~E?[1-3]~P?[37]~Y?4~L?[3568]~F?5~M?[56]~N?3~Q?[38]~R?[89]~A?[34]~J?5~D?[4-6]~Z?[89]~S?[2368]~G?[2-5]~V?9~T?[45]", "upper": "ACZ", "zip": "((?:[A-HJ-NP-Z])?\\d{4})([A-Z]{3})?", "zipex": "C1070AAM,C1000WAM,B1000TBU,X5187XAB"},
  "data/AS": {"fmt": "%N%n%O%n%A%n%C %S %Z", "id": "data/AS", "key": "AS", "name": "AMERICAN SAMOA", "posturl": "http://zip4.usps.com/zip4/welcome.jsp", "require": "ACSZ", "state_name_type": "state", "upper": "ACNOS", "zip": "(96799)(?:[ \\-](\\d{4}))?", "zip_name_type": "zip", "zipex": "96799"},
  "data/AT": {"fmt": "%O%n%N%n%A%n%Z %C", "id": "data/AT", "key": "AT", "name": "AUSTRIA", "posturl": "http://www.post.at/post_subsite_postleitzahlfinder.php", "require": "ACZ", "zip": "\\d{4}", "zipex": "1010,3741"},
  "data/AU": {"fmt": "%O%n%N%n%A%n%C %S %Z", "id": "data/AU", "key": "AU", "lang": "en", "languages": "en", "locality_name_type": "suburb", "name": "AUSTRALIA", "posturl": "http://www1.auspost.com.au/postcodes/", "require": "ACSZ", "state_name_type": "state", "sub_isoids": "ACT~NSW~NT~QLD~SA~TAS~VIC~WA", "sub_keys": "ACT~NSW~NT~QLD~SA~TAS~VIC~WA", "sub_names": "Australian Capital Territory~New South Wales~Northern Territory~Queensland~South Australia~Tasmania~Victoria~Western Australia", "sub_zipexs": "0200,2540,2618,2999~1000,2888,3585,3707~0800,0999~4000,9999~5000~7000,7999~3000,8000~6000,0872", "sub_zips": "29|2540|260|261[0-8]|02|2620~1|2[0-57-8]|26[2-9]|261[189]|3500|358[56]|3644|3707~0[89]~[49]~5|0872~7~[38]~6|0872", "upper": "CS", "zip": "\\d{4}", "zipex": "2060,3171,6430,4000,4006,3001"},
  "data/AW": {"id": "data/AW", "key": "AW", "name": "ARUBA"},
  "data/AZ": {"fmt": "%N%n%O%n%A%nAZ %Z %C", "id": "data/AZ", "key": "AZ", "name": "AZERBAIJAN", "postprefix": "AZ ", "zip": "\\d{4}", "zipex": "1000"},
  "data/BA": {"fmt": "%N%n%O%n%A%n%Z %C", "id": "data/BA", "key": "BA", "name": "BOSNIA AND HERZEGOVINA", "zip": "\\d{5}", "zipex": "71000"},
  "data/BB": {"fmt": "%N%n%O%n%A%n%C, %S %Z", "id": "data/BB", "key": "BB", "name": "BARBADOS", "state_name_type": "parish", "zip": "BB\\d{5}", "zipex": "BB23026,BB22025"},
  "data/BD": {"fmt": "%N%n%O%n%A%n%C - %Z", "id": "data/BD", "key": "BD", "name": "BANGLADESH", "posturl": "http://www.bangladeshpost.gov.bd/PostCode.asp", "zip": "\\d{4}", "zipex": "1340,1000"},
  "data/BE": {"fmt": "%O%n%N%n%A%n%Z %C", "id": "data/BE", "key": "BE", "name": "BELGIUM", "posturl": "http://www.post.be/site/nl/residential/customerservice/search/postal_codes.html", "require": "ACZ", "zip": "\\d{4}", "zipex": "4000,1000"},
  "data/BF": {"fmt": "%N%n%O%n%A%n%C %X", "id": "data/BF", "key": "BF", "name": "BURKINA FASO"},
  "data/BG": {"fmt": "%N%n%O%n%A%n%Z %C", "id": "data/BG", "key": "BG", "name": "BULGARIA (REP.)", "posturl": "http://www.bgpost.bg/?cid=5", "zip": "\\d{4}", "zipex": "1000,1700"},
  "data/BH": {"fmt": "%N%n%O%n%A%n%C %Z", "id": "data/BH", "key": "BH", "name": "BAHRAIN", "zip": "(?:\\d|1[0-2])\\d{2}", "zipex": "317"},
  "data/BI": {"id": "data/BI", "key": "BI", "name": "BURUNDI"},
  "data/BJ": {"id": "data/BJ", "key": "BJ", "name": "BENIN", "upper": "AC"},
  "data/BL": {"fmt": "%O%n%N%n%A%n%Z %C %X", "id": "data/BL", "key": "BL", "name": "SAINT BARTHELEMY", "posturl": "http://www.laposte.fr/Particulier/Utiliser-nos-outils-pratiques/Outils-et-documents/Trouvez-un-code-postal", "require": "ACZ", "upper": "ACX", "zip": "9[78][01]\\d{2}", "zipex": "97100"},
  "data/BM": {"fmt": "%N%n%O%n%A%n%C %Z", "id": "data/BM", "key": "BM", "name": "BERMUDA", "posturl": "http://www.landvaluation.bm/", "zip": "[A-Z]{2} ?[A-Z0-9]{2}", "zipex": "FL 07,HM GX,HM 12"},
  "data/BN": {"fmt": "%N%n%O%n%A%n%C %Z", "id": "data/BN", "key": "BN", "name": "BRUNEI DARUSSALAM", "posturl": "http://www.post.gov.bn/SitePages/postcodes.aspx", "zip": "[A-Z]{2} ?\\d{4}", "zipex": "BT2328,KA1131,BA1511"},
  "data/BO": {"id": "data/BO", "key": "BO", "name": "BOLIVIA", "upper": "AC"},
  "data/BQ": {"id": "data/BQ", "key": "BQ", "name": "BONAIRE, SINT EUSTATIUS, AND SABA"},
  "data/BR": {"fmt": "%O%n%N%n%A%n%D%n%C-%S%n%Z", "id": "data/BR", "key": "BR", "lang": "pt", "languages": "pt", "name": "BRAZIL", "posturl": "http://www.buscacep.correios.com.br/", "require": "ASCZ", "state_name_type": "state", "sub_isoids": "AC~AL~AP~AM~BA~CE~DF~ES~GO~MA~MT~MS~MG~PA~PB~PR~PE~PI~RJ~RN~RS~RO~RR~SC~SP~SE~TO", "sub_keys": "AC~AL~AP~AM~BA~CE~DF~ES~GO~MA~MT~MS~MG~PA~PB~PR~PE~PI~RJ~RN~RS~RO~RR~SC~SP~SE~TO", "sub_mores": "true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true", "sub_names": "Acre~Alagoas~Amapá~Amazonas~Bahia~Ceará~Distrito Federal~Espírito Santo~Goiás~Maranhão~Mato Grosso~Mato Grosso do Sul~Minas Gerais~Pará~Paraíba~Paraná~Pernambuco~Piauí~Rio de Janeiro~Rio Grande do Norte~Rio Grande do Sul~Rondônia~Roraima~Santa Catarina~São Paulo~Sergipe~Tocantins", "sub_zipexs": "69900-000,69999-999~57000-000,57999-999~68900-000,68999-999~69000-000,69400-123~40000-000,48999-999~60000-000,63999-999~70000-000,73500-123~29000-000,29999-999~72800-000,73700-123~65000-000,65999-999~78000-000,78899-999~79000-000,79999-999~30000-000,39999-999~66000-000,68899-999~58000-000,58999-999~80000-000,87999-999~50000-000,56999-999~64000-000,64999-999~20000-000,28999-999~59000-000,59999-999~90000-000,99999-999~76800-000,78900-000,78999-999~69300-000,69399-999~88000-000,89999-999~01000-000,13000-123~49000-000,49999-999~77000-000,77999-999", "sub_zips": "699~57~689~69[0-24-8]~4[0-8]~6[0-3]~7[0-1]|72[0-7]|73[0-6]~29~72[89]|73[7-9]|7[4-6]~65~78[0-8]~79~3~6[6-7]|68[0-8]~58~8[0-7]~5[0-6]~64~2[0-8]~59~9~76[89]|789~693~8[89]~[01][1-9]~49~77", "sublocality_name_type": "neighborhood", "upper": "CS", "zip": "\\d{5}-?\\d{3}", "zipex": "40301-110,70002-900"},
  "data/BS": {"fmt": "%N%n%O%n%A%n%C, %S", "id": "data/BS", "key": "BS", "lang": "en", "languages": "en", "name": "BAHAMAS", "state_name_type": "island", "sub_isoids": "~AK~~BY~BI~CI~~~EX~~HI~IN~LI~MG~~RI~RC~SS~SW", "sub_keys": "Abaco~Acklins~Andros~Berry Islands~Bimini~Cat Island~Crooked Island~Eleuthera~Exuma~Grand Bahama~Harbour Island~Inagua~Long Island~Mayaguana~N.P.~Ragged Island~Rum Cay~San Salvador~Spanish Wells", "sub_names": "Abaco Islands~Acklins~Andros Island~Berry Islands~Bimini~Cat Island~Crooked Island~Eleuthera~Exuma and Cays~Grand Bahama~Harbour Island~Inagua~Long Island~Mayaguana~New Providence~Ragged Island~Rum Cay~San Salvador~Spanish Wells"},
  "data/BT": {"fmt": "%N%n%O%n%A%n%C %Z", "id": "data/BT", "key": "BT", "name": "BHUTAN", "posturl": "http://www.bhutanpost.bt/postcodes/", "zip": "\\d{5}", "zipex": "11001,31101,35003"},
  "data/BV": {"id": "data/BV", "key": "BV", "name": "BOUVET ISLAND"},
  "data/BW": {"id": "data/BW", "key": "BW", "name": "BOTSWANA"},
  "data/BY": {"fmt": "%S%n%Z %C%n%A%n%O%n%N", "id": "data/BY", "key": "BY", "name": "BELARUS", "posturl": "http://ex.belpost.by/addressbook/", "zip": "\\d{6}", "zipex": "223016,225860,220050"},
  "data/BZ": {"id": "data/BZ", "key": "BZ", "name": "BELIZE"},
  "data/CA": {"fmt": "%N%n%O%n%A%n%C %S %Z", "id": "data/CA", "key": "CA", "lang": "en", "languages": "en~fr", "name": "CANADA", "posturl": "https://www.canadapost.ca/cpo/mc/personal/postalcode/fpc.jsf", "require": "ACSZ", "sub_isoids": "AB~BC~MB~NB~NL~NT~NS~NU~ON~PE~QC~SK~YT", "sub_keys": "AB~BC~MB~NB~NL~NT~NS~NU~ON~PE~QC~SK~YT", "sub_names": "Alberta~British Columbia~Manitoba~New Brunswick~Newfoundland and Labrador~Northwest Territories~Nova Scotia~Nunavut~Ontario~Prince Edward Island~Quebec~Saskatchewan~Yukon", "sub_zips": "T~V~R~E~A~X0E|X0G|X1A~B~X0A|X0B|X0C~K|L|M|N|P~C~G|H|J|K1A~S|R8A~Y", "upper": "ACNOSZ", "zip": "[ABCEGHJKLMNPRSTVXY]\\d[ABCEGHJ-NPRSTV-Z] ?\\d[ABCEGHJ-NPRSTV-Z]\\d", "zipex": "H3Z 2Y7,V8X 3X4,T0L 1K0,T0H 1A0,K1A 0B1"},
  "data/CA--fr": {"fmt": "%N%n%O%n%A%n%C %S %Z", "id": "data/CA--fr", "key": "CA", "lang": "fr", "name": "CANADA", "posturl": "https://www.canadapost.ca/cpo/mc/personal/postalcode/fpc.jsf", "require": "ACSZ", "sub_isoids": "AB~BC~PE~MB~NB~NS~NU~ON~QC~SK~NL~NT~YT", "sub_keys": "AB~BC~PE~MB~NB~NS~NU~ON~QC~SK~NL~NT~YT", "sub_names": "Alberta~Colombie-Britannique~Île-du-Prince-Édouard~Manitoba~Nouveau-Brunswick~Nouvelle-Écosse~Nunavut~Ontario~Québec~Saskatchewan~Terre-Neuve-et-Labrador~Territoires du Nord-Ouest~Yukon", "sub_zips": "T~V~C~R~E~B~X0A|X0B|X0C~K|L|M|N|P~G|H|J|K1A~S|R8A~A~X0E|X0G|X1A~Y", "upper": "ACNOSZ", "zip": "[ABCEGHJKLMNPRSTVXY]\\d[ABCEGHJ-NPRSTV-Z] ?\\d[ABCEGHJ-NPRSTV-Z]\\d", "zipex": "H3Z 2Y7,V8X 3X4,T0L 1K0,T0H 1A0,K1A 0B1"},
  "data/CC": {"fmt": "%O%n%N%n%A%n%C %S %Z", "id": "data/CC", "key": "CC", "name": "COCOS (KEELING) ISLANDS", "upper": "CS", "zip": "6799", "zipex": "6799"},
  "data/CD": {"id": "data/CD", "key": "CD", "name": "CONGO (DEM. REP.)"},
  "data/CF": {"id": "data/CF", "key": "CF", "name": "CENTRAL AFRICAN REPUBLIC"},
  "data/CG": {"id": "data/CG", "key": "CG", "name": "CONGO (REP.)"},
  "data/CH": {"fmt": "%O%n%N%n%A%nCH-%Z %C", "id": "data/CH", "key": "CH", "name": "SWITZERLAND", "postprefix": "CH-", "posturl": "http://www.post.ch/db/owa/pv_plz_pack/pr_main", "require": "ACZ", "upper": "", "zip": "\\d{4}", "zipex": "2544,1211,1556,3030"},
  "data/CI": {"fmt": "%N%n%O%n%X %A %C %X", "id": "data/CI", "key": "CI", "name": "COTE D'IVOIRE"},
  "data/CK": {"id": "data/CK", "key": "CK", "name": "COOK ISLANDS"},
  "data/CL": {"fmt": "%N%n%O%n%A%n%Z %C%n%S", "id": "data/CL", "key": "CL", "lang": "es", "languages": "es", "name": "CHILE", "posturl": "http://www.correos.cl/SitePages/home.aspx", "sub_isoids": "AN~AR~AP~AT~AI~BI~CO~LI~LL~LR~MA~ML~RM~TA~VS", "sub_keys": "Antofagasta~Araucanía~Arica y Parinacota~Atacama~Aysén~Biobío~Coquimbo~O'Higgins~Los Lagos~Los Ríos~Magallanes~Maule~Región Metropolitana~Tarapacá~Valparaíso", "sub_mores": "true~true~true~true~true~true~true~true~true~true~true~true~true~true~true", "sub_names": "Antofagasta~Araucanía~Arica y Parinacota~Atacama~Aysén del General Carlos Ibáñez del Campo~Biobío~Coquimbo~Libertador General Bernardo O'Higgins~Los Lagos~Los Ríos~Magallanes y de la Antártica Chilena~Maule~Metropolitana de Santiago~Tarapacá~Valparaíso", "zip": "\\d{7}", "zipex": "8340457,8720019,1230000,8329100"},
  "data/CM": {"id": "data/CM", "key": "CM", "name": "CAMEROON"},
  "data/CN": {"fmt": "%Z%n%S%C%D%n%A%n%O%n%N", "id": "data/CN", "key": "CN", "lang": "zh", "languages": "zh", "lfmt": "%N%n%O%n%A%n%D%n%C%n%S, %Z", "name": "CHINA", "posturl": "http://www.ems.com.cn/serviceguide/you_bian_cha_xun.html", "require": "ACSZ", "sub_isoids": "34~92~11~50~35~62~44~45~52~46~13~41~23~42~43~22~32~36~21~15~64~63~37~14~61~31~51~71~12~54~91~65~53~33", "sub_keys": "安徽省~澳门~北京市~重庆市~福建省~甘肃省~广东省~广西壮族自治区~贵州省~海南省~河北省~河南省~黑龙江省~湖北省~湖南省~吉林省~江苏省~江西省~辽宁省~内蒙古自治区~宁夏回族自治区~青海省~山东省~山西省~陕西省~上海市~四川省~台湾~天津市~西藏自治区~香港~新疆维吾尔自治区~云南省~浙江省", "sub_lnames": "Anhui Sheng~Macau~Beijing Shi~Chongqing Shi~Fujian Sheng~Gansu Sheng~Guangdong Sheng~Guangxi Zhuangzuzizhiqu~Guizhou Sheng~Hainan Sheng~Hebei Sheng~Henan Sheng~Heilongjiang Sheng~Hubei Sheng~Hunan Sheng~Jilin Sheng~Jiangsu Sheng~Jiangxi Sheng~Liaoning Sheng~Neimenggu Zizhiqu~Ningxia Huizuzizhiqu~Qinghai Sheng~Shandong Sheng~Shanxi Sheng~Shaanxi Sheng~Shanghai Shi~Sichuan Sheng~Taiwan~Tianjin Shi~Xizang Zizhiqu~Hong Kong~Xinjiang Weiwuerzizhiqu~Yunnan Sheng~Zhejiang Sheng", "sub_mores": "true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true", "sub_names": "安徽省~澳门~北京市~重庆市~福建省~甘肃省~广东省~广西~贵州省~海南省~河北省~河南省~黑龙江省~湖北省~湖南省~吉林省~江苏省~江西省~辽宁省~内蒙古~宁夏~青海省~山东省~山西省~陕西省~上海市~四川省~台湾~天津市~西藏~香港~新疆~云南省~浙江省", "sub_xrequires": "~A~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ACS~~~", "sub_xzips": "~999078~~~~~~~~~~~~~~~~~~~~~~~~~~\\d{3}(\\d{2})?~~~999077~~~", "sublocality_name_type": "district", "upper": "S", "zip": "\\d{6}", "zipex": "266033,317204,100096,100808"},
  "data/CO": {"fmt": "%N%n%O%n%A%n%C, %S, %Z", "id": "data/CO", "key": "CO", "name": "COLOMBIA", "posturl": "http://www.codigopostal.gov.co/", "require": "AS", "state_name_type": "department", "zip": "\\d{6}", "zipex": "111221,130001,760011"},
  "data/CR": {"fmt": "%N%n%O%n%A%n%S, %C%n%Z", "id": "data/CR", "key": "CR", "name": "COSTA RICA", "posturl": "https://www.correos.go.cr/nosotros/codigopostal/busqueda.html", "require": "ACS", "zip": "\\d{4,5}|\\d{3}-\\d{4}", "zipex": "1000,2010,1001"},
  "data/CU": {"fmt": "%N%n%O%n%A%n%C %S%n%Z", "id": "data/CU", "key": "CU", "lang": "es", "languages": "es", "name": "CUBA", "sub_isoids": "15~09~08~06~12~14~11~99~03~10~04~16~01~07~13~05", "sub_keys": "Artemisa~Camagüey~Ciego de Ávila~Cienfuegos~Granma~Guantánamo~Holguín~Isla de la Juventud~La Habana~Las Tunas~Matanzas~Mayabeque~Pinar del Río~Sancti Spíritus~Santiago de Cuba~Villa Clara", "zip": "\\d{5}", "zipex": "10700"},
  "data/CV": {"fmt": "%N%n%O%n%A%n%Z %C%n%S", "id": "data/CV", "key": "CV", "lang": "pt", "languages": "pt", "name": "CAPE VERDE", "state_name_type": "island", "sub_isoids": "BV~BR~~MA~SL~~~~SV", "sub_keys": "Boa Vista~Brava~Fogo~Maio~Sal~Santiago~Santo Antão~São Nicolau~São Vicente", "zip": "\\d{4}", "zipex": "7600"},
  "data/CW": {"id": "data/CW", "key": "CW", "name": "CURACAO"},
  "data/CX": {"fmt": "%O%n%N%n%A%n%C %S %Z", "id": "data/CX", "key": "CX", "name": "CHRISTMAS ISLAND", "upper": "CS", "zip": "6798", "zipex": "6798"},
  "data/CY": {"fmt": "%N%n%O%n%A%n%Z %C", "id": "data/CY", "key": "CY", "name": "CYPRUS", "zip": "\\d{4}", "zipex": "2008,3304,1900"},
  "data/CZ": {"fmt": "%N%n%O%n%A%n%Z %C", "id": "data/CZ", "key": "CZ", "name": "CZECH REP.", "posturl": "http://psc.ceskaposta.cz/CleanForm.action", "require": "ACZ", "zip": "\\d{3} ?\\d{2}", "zipex": "100 00,251 66,530 87,110 00,225 99"},
  "data/DE": {"fmt": "%N%n%O%n%A%n%Z %C", "id": "data/DE", "key": "DE", "name": "GERMANY", "posturl": "http://www.postdirekt.de/plzserver/", "require": "ACZ", "zip": "\\d{5}", "zipex": "26133,53225"},
  "data/DJ": {"id": "data/DJ", "key": "DJ", "name": "DJIBOUTI"},
  "data/DK": {"fmt": "%N%n%O%n%A%n%Z %C", "id": "data/DK", "key": "DK", "name": "DENMARK", "posturl": "http://www.postdanmark.dk/da/Privat/Kundeservice/postnummerkort/Sider/Find-postnummer.aspx", "require": "ACZ", "zip": "\\d{4}", "zipex": "8660,1566"},
  "data/DM": {"id": "data/DM", "key": "DM", "name": "DOMINICA"},
  "data/DO": {"fmt": "%N%n%O%n%A%n%Z %C", "id": "data/DO", "key": "DO", "name": "DOMINICAN REP.", "posturl": "http://inposdom.gob.do/codigo-postal/", "zip": "\\d{5}", "zipex": "11903,10101"},
  "data/DZ": {"fmt": "%N%n%O%n%A%n%Z %C", "id": "data/DZ", "key": "DZ", "name": "ALGERIA", "zip": "\\d{5}", "zipex": "40304,16027"},
  "data/EC": {"fmt": "%N%n%O%n%A%n%Z%n%C", "id": "data/EC", "key": "EC", "name": "ECUADOR", "posturl": "http://www.codigopostal.gob.ec/", "upper": "CZ", "zip": "\\d{6}", "zipex": "090105,092301"},
  "data/EE": {"fmt": "%N%n%O%n%A%n%Z %C", "id": "data/EE", "key": "EE", "name": "ESTONIA", "posturl": "https://www.omniva.ee/era/sihtnumbrite_otsing", "zip": "\\d{5}", "zipex": "69501,11212"},
  "data/EG": {"fmt": "%N%n%O%n%A%n%C%n%S%n%Z", "id": "data/EG", "key": "EG", "lang": "ar", "languages": "ar", "lfmt": "%N%n%O%n%A%n%C%n%S%n%Z", "name": "EGYPT", "sub_isoids": "ASN~AST~ALX~IS~LX~BA~BH~GZ~DK~SUZ~SHR~GH~FYM~C~KB~MNF~MN~WAD~BNS~PTS~JS~DT~SHG~SIN~KN~KFS~MT", "sub_keys": "أسوان~أسيوط~الإسكندرية~الإسماعيلية~الأقصر~البحر الأحمر~البحيرة~الجيزة~الدقهلية~السويس~الشرقية~الغربية~الفيوم~القاهرة~القليوبية~المنوفية~المنيا~الوادي الجديد~بني سويف~بورسعيد~جنوب سيناء~دمياط~سوهاج~شمال سيناء~قنا~كفر الشيخ~مطروح", "sub_lnames": "Aswan Governorate~Asyut Governorate~Alexandria Governorate~Ismailia Governorate~Luxor Governorate~Red Sea Governorate~El Beheira Governorate~Giza Governorate~Dakahlia Governorate~Suez Governorate~Ash Sharqia Governorate~Gharbia Governorate~Faiyum Governorate~Cairo Governorate~Qalyubia Governorate~Menofia Governorate~Menia Governorate~New Valley Governorate~Beni Suef Governorate~Port Said Governorate~South Sinai Governorate~Damietta Governorate~Sohag Governorate~North Sinai Governorate~Qena Governorate~Kafr El Sheikh Governorate~Matrouh Governorate", "sub_zipexs": "81000~71000~21000,23000~41000~85000~84000~22000~12000~35000~43000~44000~31000~63000~11000~13000~32000~61000~72000~62000~42000~46000~34000~82000~45000~83000~33000~51000", "sub_zips": "81~71~2[13]~41~85~84~22~12~35~43~44~31~63~11~13~32~61~72~62~42~46~34~82~45~83~33~51", "zip": "\\d{5}", "zipex": "12411,11599"},
  "data/EH": {"fmt": "%N%n%O%n%A%n%Z %C", "id": "data/EH", "key": "EH", "name": "WESTERN SAHARA", "zip": "\\d{5}", "zipex": "70000,72000"},
  "data/ER": {"id": "data/ER", "key": "ER", "name": "ERITREA"},
  "data/ES": {"fmt": "%N%n%O%n%A%n%Z %C %S", "id": "data/ES", "key": "ES", "lang": "es", "languages": "es~ca~gl~eu", "name": "SPAIN", "posturl": "http://www.correos.es/contenido/13-MenuRec2/04-MenuRec24/1010_s-CodPostal.asp", "require": "ACSZ", "sub_keys": "VI~AB~A~AL~O~AV~BA~B~BU~CC~CA~S~CS~CE~CR~CO~CU~GI~GR~GU~SS~H~HU~PM~J~C~LO~GC~LE~L~LU~M~MA~ML~MU~NA~OR~P~PO~SA~TF~SG~SE~SO~T~TE~TO~V~VA~BI~ZA~Z", "sub_names": "Álava~Albacete~Alicante~Almería~Asturias~Ávila~Badajoz~Barcelona~Burgos~Cáceres~Cádiz~Cantabria~Castellón~Ceuta~Ciudad Real~Córdoba~Cuenca~Girona~Granada~Guadalajara~Guipúzcoa~Huelva~Huesca~Islas Baleares~Jaén~La Coruña~La Rioja~Las Palmas~León~Lérida~Lugo~Madrid~Málaga~Melilla~Murcia~Navarra~Ourense~Palencia~Pontevedra~Salamanca~Santa Cruz de Tenerife~Segovia~Sevilla~Soria~Tarragona~Teruel~Toledo~Valencia~Valladolid~Vizcaya~Zamora~Zaragoza", "sub_zips": "01~02~03~04~33~05~06~08~09~10~11~39~12~51~13~14~16~17~18~19~20~21~22~07~23~15~26~35~24~25~27~28~29~52~30~31~32~34~36~37~38~40~41~42~43~44~45~46~47~48~49~50", "upper": "CS", "zip": "\\d{5}", "zipex": "28039,28300,28070"},
  "data/ES--ca": {"fmt": "%N%n%O%n%A%n%Z %C %S", "id": "data/ES--ca", "key": "ES", "lang": "ca", "name": "SPAIN", "posturl": "http://www.correos.es/contenido/13-MenuRec2/04-MenuRec24/1010_s-CodPostal.asp", "require": "ACSZ", "sub_keys": "A~AB~AL~VI~O~AV~BA~B~BI~BU~CC~CA~S~CS~CE~CR~CO~CU~GI~GR~GU~SS~H~HU~PM~J~C~LO~GC~LE~L~LU~M~MA~ML~MU~NA~OR~P~PO~SA~TF~SG~SE~SO~T~TE~TO~V~VA~ZA~Z", "sub_names": "Alacant~Albacete~Almeria~Araba~Asturias~Àvila~Badajoz~Barcelona~Bizkaia~Burgos~Cáceres~Cadis~Cantabria~Castelló~Ceuta~Ciudad Real~Córdoba~Cuenca~Girona~Granada~Guadalajara~Guipúscoa~Huelva~Huesca~Illes Balears~Jaén~La Corunya~La Rioja~Las Palmas~León~Lleida~Lugo~Madrid~Málaga~Melilla~Murcia~Navarra~Ourense~Palencia~Pontevedra~Salamanca~Santa Cruz de Tenerife~Segovia~Sevilla~Soria~Tarragona~Teruel~Toledo~València~Valladolid~Zamora~Zaragoza", "sub_zips": "03~02~04~01~33~05~06~08~48~09~10~11~39~12~51~13~14~16~17~18~19~20~21~22~07~23~15~26~35~24~25~27~28~29~52~30~31~32~34~36~37~38~40~41~42~43~44~45~46~47~49~50", "upper": "CS", "zip": "\\d{5}", "zipex": "28039,28300,28070"},
  "data/ES--eu": {"fmt": "%N%n%O%n%A%n%Z %C %S", "id": "data/ES--eu", "key": "ES", "lang": "eu", "name": "SPAIN", "posturl": "http://www.correos.es/contenido/13-MenuRec2/04-MenuRec24/1010_s-CodPostal.asp", "require": "ACSZ", "sub_keys": "A~AB~AL~VI~O~AV~BA~B~BI~BU~CC~CA~S~CS~CE~CR~C~CU~SS~GI~GR~GU~H~HU~PM~J~CO~LO~GC~LE~L~LU~M~MA~ML~MU~NA~OR~P~PO~SA~TF~SG~SE~SO~T~TE~TO~V~VA~ZA~Z", "sub_names": "Alacant~Albacete~Almería~Araba~Asturias~Ávila~Badajoz~Barcelona~Bizkaia~Burgos~Cáceres~Cádiz~Cantabria~Castelló~Ceuta~Ciudad Real~Coruña~Cuenca~Gipuzkoa~Girona~Granada~Guadalajara~Huelva~Huesca~Illes Balears~Jaén~Kordoba~La Rioja~Las Palmas~León~Lleida~Lugo~Madrid~Málaga~Melilla~Murtzia~Nafarroa~Ourense~Palentzia~Pontevedra~Salamanca~Santa Cruz Tenerifekoa~Segovia~Sevilla~Soria~Tarragona~Teruel~Toledo~Valentzia~Valladolid~Zamora~Zaragoza", "sub_zips": "03~02~04~01~33~05~06~08~48~09~10~11~39~12~51~13~15~16~20~17~18~19~21~22~07~23~14~26~35~24~25~27~28~29~52~30~31~32~34~36~37~38~40~41~42~43~44~45~46~47~49~50", "upper": "CS", "zip": "\\d{5}", "zipex": "28039,28300,28070"},
  "data/ES--gl": {"fmt": "%N%n%O%n%A%n%Z %C %S", "id": "data/ES--gl", "key": "ES", "lang": "gl", "name": "SPAIN", "posturl": "http://www.correos.es/contenido/13-MenuRec2/04-MenuRec24/1010_s-CodPostal.asp", "require": "ACSZ", "sub_keys": "C~A~VI~AB~AL~GC~O~AV~BA~B~BI~BU~CC~CA~S~CS~CE~CR~CO~CU~GR~GU~SS~H~HU~PM~LO~LE~L~LU~M~MA~ML~MU~NA~OR~P~PO~SA~TF~SG~SE~SO~T~TE~TO~V~VA~J~GI~ZA~Z", "sub_names": "A Coruña~Alacant~Álava~Albacete~Almería~As Palmas~Asturias~Ávila~Badaxoz~Barcelona~Biscaia~Burgos~Cáceres~Cádiz~Cantabria~Castelló~Ceuta~Cidade Real~Córdoba~Cuenca~Granada~Guadalajara~Guipúscoa~Huelva~Huesca~Illas Baleares~La Rioja~León~Lleida~Lugo~Madrid~Málaga~Melilla~Murcia~Navarra~Ourense~Palencia~Pontevedra~Salamanca~Santa Cruz de Tenerife~Segovia~Sevilla~Soria~Tarragona~Teruel~Toledo~Valencia~Valladolid~Xaén~Xirona~Zamora~Zaragoza", "sub_zips": "15~03~01~02~04~35~33~05~06~08~48~09~10~11~39~12~51~13~14~16~18~19~20~21~22~07~26~24~25~27~28~29~52~30~31~32~34~36~37~38~40~41~42~43~44~45~46~47~23~17~49~50", "upper": "CS", "zip": "\\d{5}", "zipex": "28039,28300,28070"},
  "data/ET": {"fmt": "%N%n%O%n%A%n%Z %C", "id": "data/ET", "key": "ET", "name": "ETHIOPIA", "zip": "\\d{4}", "zipex": "1000"},
  "data/FI": {"fmt": "%O%n%N%n%A%nFI-%Z %C", "id": "data/FI", "key": "FI", "name": "FINLAND", "postprefix": "FI-", "posturl": "http://www.verkkoposti.com/e3/postinumeroluettelo", "require": "ACZ", "zip": "\\d{5}", "zipex": "00550,00011"},
  "data/FJ": {"id": "data/FJ", "key": "FJ", "name": "FIJI"},
  "data/FK": {"fmt": "%N%n%O%n%A%n%C%n%Z", "id": "data/FK", "key": "FK", "name": "FALKLAND ISLANDS (MALVINAS)", "require": "ACZ", "upper": "CZ", "zip": "FIQQ 1ZZ", "zipex": "FIQQ 1ZZ"},
  "data/FM": {"fmt": "%N%n%O%n%A%n%C %S %Z", "id": "data/FM", "key": "FM", "name": "MICRONESIA (Federated State of)", "posturl": "http://zip4.usps.com/zip4/welcome.jsp", "require": "ACSZ", "state_name_type": "state", "upper": "ACNOS", "zip": "(9694[1-4])(?:[ \\-](\\d{4}))?", "zip_name_type": "zip", "zipex": "96941,96944"},
  "data/FO": {"fmt": "%N%n%O%n%A%nFO%Z %C", "id": "data/FO", "key": "FO", "name": "FAROE ISLANDS", "postprefix": "FO", "posturl": "http://www.postur.fo/", "zip": "\\d{3}", "zipex": "100"},
  "data/FR": {"fmt": "%O%n%N%n%A%n%Z %C %X", "id": "data/FR", "key": "FR", "name": "FRANCE", "posturl": "http://www.laposte.fr/Particulier/Utiliser-nos-outils-pratiques/Outils-et-documents/Trouvez-un-code-postal", "require": "ACZ", "upper": "CX", "zip": "\\d{2} ?\\d{3}", "zipex": "33380,34092,33506"},
  "data/GA": {"id": "data/GA", "key": "GA", "name": "GABON"},
  "data/GB": {"fmt": "%N%n%O%n%A%n%C%n%Z", "id": "data/GB", "key": "GB", "locality_name_type": "post_town", "name": "UNITED KINGDOM", "posturl": "http://www.royalmail.com/postcode-finder", "require": "ACZ", "upper": "CZ", "zip": "GIR ?0AA|(?:(?:AB|AL|B|BA|BB|BD|BF|BH|BL|BN|BR|BS|BT|BX|CA|CB|CF|CH|CM|CO|CR|CT|CV|CW|DA|DD|DE|DG|DH|DL|DN|DT|DY|E|EC|EH|EN|EX|FK|FY|G|GL|GY|GU|HA|HD|HG|HP|HR|HS|HU|HX|IG|IM|IP|IV|JE|KA|KT|KW|KY|L|LA|LD|LE|LL|LN|LS|LU|M|ME|MK|ML|N|NE|NG|NN|NP|NR|NW|OL|OX|PA|PE|PH|PL|PO|PR|RG|RH|RM|S|SA|SE|SG|SK|SL|SM|SN|SO|SP|SR|SS|ST|SW|SY|TA|TD|TF|TN|TQ|TR|TS|TW|UB|W|WA|WC|WD|WF|WN|WR|WS|WV|YO|ZE)(?:\\d[\\dA-Z]? ?\\d[ABD-HJLN-UW-Z]{2}))|BFPO ?\\d{1,4}", "zipex": "EC1Y 8SY,GIR 0AA,M2 5BQ,M34 4AB,CR0 2YR,DN16 9AA,W1A 4ZZ,EC1A 1HQ,OX14 4PG,BS18 8HF,NR25 7HG,RH6 0NP,BH23 6AA,B6 5BA,SO23 9AP,PO1 3AX,BFPO 61"},
  "data/GD": {"id": "data/GD", "key": "GD", "name": "GRENADA (WEST INDIES)"},
  "data/GE": {"fmt": "%N%n%O%n%A%n%Z %C", "id": "data/GE", "key": "GE", "name": "GEORGIA", "posturl": "http://www.georgianpost.ge/index.php?page=10", "zip": "\\d{4}", "zipex": "0101"},
  "data/GF": {"fmt": "%O%n%N%n%A%n%Z %C %X", "id": "data/GF", "key": "GF", "name": "FRENCH GUIANA", "posturl": "http://www.laposte.fr/Particulier/Utiliser-nos-outils-pratiques/Outils-et-documents/Trouvez-un-code-postal", "require": "ACZ", "upper": "ACX", "zip": "9[78]3\\d{2}", "zipex": "97300"},
  "data/GG": {"fmt": "%N%n%O%n%A%n%C%nGUERNSEY%n%Z", "id": "data/GG", "key": "GG", "name": "CHANNEL ISLANDS", "posturl": "http://www.guernseypost.com/postcode_finder/", "require": "ACZ", "upper": "CZ", "zip": "GY\\d[\\dA-Z]? ?\\d[ABD-HJLN-UW-Z]{2}", "zipex": "GY1 1AA,GY2 2BT"},
  "data/GH": {"id": "data/GH", "key": "GH", "name": "GHANA"},
  "data/GI": {"fmt": "%N%n%O%n%A%nGIBRALTAR%n%Z", "id": "data/GI", "key": "GI", "name": "GIBRALTAR", "require": "A", "zip": "GX11 1AA", "zipex": "GX11 1AA"},
  "data/GL": {"fmt": "%N%n%O%n%A%n%Z %C", "id": "data/GL", "key": "GL", "name": "GREENLAND", "require": "ACZ", "zip": "39\\d{2}", "zipex": "3900,3950,3911"},
  "data/GM": {"id": "data/GM", "key": "GM", "name": "GAMBIA"},
  "data/GN": {"fmt": "%N%n%O%n%Z %A %C", "id": "data/GN", "key": "GN", "name": "GUINEA", "zip": "\\d{3}", "zipex": "001,200,100"},
  "data/GP": {"fmt": "%O%n%N%n%A%n%Z %C %X", "id": "data/GP", "key": "GP", "name": "GUADELOUPE", "posturl": "http://www.laposte.fr/Particulier/Utiliser-nos-outils-pratiques/Outils-et-documents/Trouvez-un-code-postal", "require": "ACZ", "upper": "ACX", "zip": "9[78][01]\\d{2}", "zipex": "97100"},
  "data/GQ": {"id": "data/GQ", "key": "GQ", "name": "EQUATORIAL GUINEA"},
  "data/GR": {"fmt": "%N%n%O%n%A%n%Z %C", "id": "data/GR", "key": "GR", "name": "GREECE", "posturl": "http://www.elta.gr/findapostcode.aspx", "require": "ACZ", "zip": "\\d{3} ?\\d{2}", "zipex": "151 24,151 10,101 88"},
  "data/GS": {"fmt": "%N%n%O%n%A%n%n%C%n%Z", "id": "data/GS", "key": "GS", "name": "SOUTH GEORGIA", "require": "ACZ", "upper": "CZ", "zip": "SIQQ 1ZZ", "zipex": "SIQQ 1ZZ"},
  "data/GT": {"fmt": "%N%n%O%n%A%n%Z- %C", "id": "data/GT", "key": "GT", "name": "GUATEMALA", "zip": "\\d{5}", "zipex": "09001,01501"},
  "data/GU": {"fmt": "%N%n%O%n%A%n%C %Z", "id": "data/GU", "key": "GU", "name": "GUAM", "posturl": "http://zip4.usps.com/zip4/welcome.jsp", "require": "ACZ", "upper": "ACNO", "zip": "(969(?:[12]\\d|3[12]))(?:[ \\-](\\d{4}))?", "zip_name_type": "zip", "zipex": "96910,96931"},
  "data/GW": {"fmt": "%N%n%O%n%A%n%Z %C", "id": "data/GW", "key": "GW", "name": "GUINEA-BISSAU", "zip": "\\d{4}", "zipex": "1000,1011"},
  "data/GY": {"id": "data/GY", "key": "GY", "name": "GUYANA"},
  "data/HK": {"fmt": "%S%n%C%n%A%n%O%n%N", "id": "data/HK", "key": "HK", "lang": "zh-Hant", "languages": "zh-Hant~en", "lfmt": "%N%n%O%n%A%n%C%n%S", "locality_name_type": "district", "name": "HONG KONG", "require": "AS", "state_name_type": "area", "sub_keys": "Kowloon~Hong Kong Island~New Territories", "sub_mores": "true~true~true", "sub_names": "九龍~香港島~新界", "upper": "S"},
  "data/HK--en": {"fmt": "%S%n%C%n%A%n%O%n%N", "id": "data/HK--en", "key": "HK", "lang": "en", "lfmt": "%N%n%O%n%A%n%C%n%S", "locality_name_type": "district", "name": "HONG KONG", "require": "AS", "state_name_type": "area", "sub_keys": "Hong Kong Island~Kowloon~New Territories", "sub_lnames": "Hong Kong Island~Kowloon~New Territories", "sub_mores": "true~true~true", "upper": "S"},
  "data/HM": {"fmt": "%O%n%N%n%A%n%C %S %Z", "id": "data/HM", "key": "HM", "name": "HEARD AND MCDONALD ISLANDS", "upper": "CS", "zip": "\\d{4}", "zipex": "7050"},
  "data/HN": {"fmt": "%N%n%O%n%A%n%C, %S%n%Z", "id": "data/HN", "key": "HN", "name": "HONDURAS", "require": "ACS", "zip": "\\d{5}", "zipex": "31301"},
  "data/HR": {"fmt": "%N%n%O%n%A%nHR-%Z %C", "id": "data/HR", "key": "HR", "name": "CROATIA", "postprefix": "HR-", "posturl": "http://www.posta.hr/default.aspx?pretpum", "zip": "\\d{5}", "zipex": "10000,21001,10002"},
  "data/HT": {"fmt": "%N%n%O%n%A%nHT%Z %C", "id": "data/HT", "key": "HT", "name": "HAITI", "postprefix": "HT", "zip": "\\d{4}", "zipex": "6120,5310,6110,8510"},
  "data/HU": {"fmt": "%N%n%O%n%C%n%A%n%Z", "id": "data/HU", "key": "HU", "name": "HUNGARY (Rep.)", "posturl": "http://posta.hu/ugyfelszolgalat/iranyitoszam_kereso", "require": "ACZ", "upper": "ACNO", "zip": "\\d{4}", "zipex": "1037,2380,1540"},
  "data/ID": {"fmt": "%N%n%O%n%A%n%C%n%S %Z", "id": "data/ID", "key": "ID", "lang": "id", "languages": "id", "name": "INDONESIA", "require": "AS", "sub_isoids": "AC~BA~BT~BE~YO~JK~GO~JA~JB~JT~JI~KB~KS~KT~KI~KU~BB~KR~LA~MA~MU~NB~NT~PA~PB~RI~SR~SN~ST~SG~SA~SB~SS~SU", "sub_keys": "Aceh~Bali~Banten~Bengkulu~Daerah Istimewa Yogyakarta~DKI Jakarta~Gorontalo~Jambi~Jawa Barat~Jawa Tengah~Jawa Timur~Kalimantan Barat~Kalimantan Selatan~Kalimantan Tengah~Kalimantan Timur~Kalimantan Utara~Kepulauan Bangka Belitung~Kepulauan Riau~Lampung~Maluku~Maluku Utara~Nusa Tenggara Barat~Nusa Tenggara Timur~Papua~Papua Barat~Riau~Sulawesi Barat~Sulawesi Selatan~Sulawesi Tengah~Sulawesi Tenggara~Sulawesi Utara~Sumatera Barat~Sumatera Selatan~Sumatera Utara", "zip": "\\d{5}", "zipex": "40115"},
  "data/IE": {"fmt": "%N%n%O%n%A%n%D%n%C%n%S %Z", "id": "data/IE", "key": "IE", "lang": "en", "languages": "en", "name": "IRELAND", "posturl": "https://finder.eircode.ie", "state_name_type": "county", "sub_isoids": "CW~CN~CE~C~DL~D~G~KY~KE~KK~LS~LM~LK~LD~LH~MO~MH~MN~OY~RN~SO~TA~WD~WH~WX~WW", "sub_keys": "Co. Carlow~Co. Cavan~Co. Clare~Co. Cork~Co. Donegal~Co. Dublin~Co. Galway~Co. Kerry~Co. Kildare~Co. Kilkenny~Co. Laois~Co. Leitrim~Co. Limerick~Co. Longford~Co. Louth~Co. Mayo~Co. Meath~Co. Monaghan~Co. Offaly~Co. Roscommon~Co. Sligo~Co. Tipperary~Co. Waterford~Co. Westmeath~Co. Wexford~Co. Wicklow", "sublocality_name_type": "townland", "zip": "[\\dA-Z]{3} ?[\\dA-Z]{4}", "zip_name_type": "eircode", "zipex": "A65 F4E2"},
  "data/IL": {"fmt": "%N%n%O%n%A%n%C %Z", "id": "data/IL", "key": "IL", "name": "ISRAEL", "posturl": "http://www.israelpost.co.il/zipcode.nsf/demozip?openform", "zip": "\\d{5}(?:\\d{2})?", "zipex": "9614303"},
  "data/IM": {"fmt": "%N%n%O%n%A%n%C%n%Z", "id": "data/IM", "key": "IM", "name": "ISLE OF MAN", "posturl": "https://www.iompost.com/tools-forms/postcode-finder/", "require": "ACZ", "upper": "CZ", "zip": "IM\\d[\\dA-Z]? ?\\d[ABD-HJLN-UW-Z]{2}", "zipex": "IM2 1AA,IM99 1PS"},
  "data/IN": {"fmt": "%N%n%O%n%A%n%C %Z%n%S", "id": "data/IN", "key": "IN", "lang": "en", "languages": "en~hi", "name": "INDIA", "posturl": "https://www.indiapost.gov.in/vas/pages/FindPinCode.aspx", "require": "ACSZ", "state_name_type": "state", "sub_isoids": "AN~AP~AR~AS~BR~CH~CT~DN~DD~DL~GA~GJ~HR~HP~JK~JH~KA~KL~LD~MP~MH~MN~ML~MZ~NL~OR~PY~PB~RJ~SK~TN~TG~TR~UP~UT~WB", "sub_keys": "Andaman and Nicobar Islands~Andhra Pradesh~Arunachal Pradesh~Assam~Bihar~Chandigarh~Chhattisgarh~Dadra and Nagar Haveli~Daman and Diu~Delhi~Goa~Gujarat~Haryana~Himachal Pradesh~Jammu and Kashmir~Jharkhand~Karnataka~Kerala~Lakshadweep~Madhya Pradesh~Maharashtra~Manipur~Meghalaya~Mizoram~Nagaland~Odisha~Puducherry~Punjab~Rajasthan~Sikkim~Tamil Nadu~Telangana~Tripura~Uttar Pradesh~Uttarakhand~West Bengal", "sub_names": "Andaman & Nicobar~Andhra Pradesh~Arunachal Pradesh~Assam~Bihar~Chandigarh~Chhattisgarh~Dadra & Nagar Haveli~Daman & Diu~Delhi~Goa~Gujarat~Haryana~Himachal Pradesh~Jammu & Kashmir~Jharkhand~Karnataka~Kerala~Lakshadweep~Madhya Pradesh~Maharashtra~Manipur~Meghalaya~Mizoram~Nagaland~Odisha~Puducherry~Punjab~Rajasthan~Sikkim~Tamil Nadu~Telangana~Tripura~Uttar Pradesh~Uttarakhand~West Bengal", "sub_zips": "744~5[0-3]~79[0-2]~78~8[0-5]~16|1440[3-9]~49~396~396~11~403~3[6-9]~1[23]~17~1[89]~81[4-9]|82|83[0-5]~5[4-9]|53[7-9]~6[7-9]|6010|607008|777~682~4[5-8]|490~4[0-4]~79[56]~79[34]~796~79[78]~7[5-7]~60[579]~1[456]~3[0-4]~737|750~6[0-6]|536~5[0-3]~799~2[0-35-8]|24[0-7]|26[12]~24[46-9]|254|26[23]~7[0-4]", "zip": "\\d{6}", "zip_name_type": "pin", "zipex": "110034,110001"},
  "data/IN--hi": {"fmt": "%N%n%O%n%A%n%C %Z%n%S", "id": "data/IN--hi", "key": "IN", "lang": "hi", "name": "INDIA", "posturl": "https://www.indiapost.gov.in/vas/pages/FindPinCode.aspx", "require": "ACSZ", "state_name_type": "state", "sub_isoids": "AN~AR~AS~AP~UP~UT~OR~KA~KL~GJ~GA~CH~CT~JK~JH~TN~TG~TR~DD~DN~DL~NL~PB~WB~PY~BR~MN~MP~MH~MZ~ML~RJ~LD~SK~HR~HP", "sub_keys": "Andaman & Nicobar~Arunachal Pradesh~Assam~Andhra Pradesh~Uttar Pradesh~Uttarakhand~Odisha~Karnataka~Kerala~Gujarat~Goa~Chandigarh~Chhattisgarh~Jammu & Kashmir~Jharkhand~Tamil Nadu~Telangana~Tripura~Daman & Diu~Dadra & Nagar Haveli~Delhi~Nagaland~Punjab~West Bengal~Puducherry~Bihar~Manipur~Madhya Pradesh~Maharashtra~Mizoram~Meghalaya~Rajasthan~Lakshadweep~Sikkim~Haryana~Himachal Pradesh", "sub_names": "अंडमान और निकोबार द्वीपसमूह~अरुणाचल प्रदेश~असम~आंध्र प्रदेश~उत्तर प्रदेश~उत्तराखण्ड~ओड़िशा~कर्नाटक~केरल~गुजरात~गोआ~चंडीगढ़~छत्तीसगढ़~जम्मू और कश्मीर~झारखण्ड~तमिल नाडु~तेलंगाना~त्रिपुरा~दमन और दीव~दादरा और नगर हवेली~दिल्ली~नागालैंड~पंजाब~पश्चिम बंगाल~पांडिचेरी~बिहार~मणिपुर~मध्य प्रदेश~महाराष्ट्र~मिजोरम~मेघालय~राजस्थान~लक्षद्वीप~सिक्किम~हरियाणा~हिमाचल प्रदेश", "sub_zips": "744~79[0-2]~78~5[0-3]~2[0-35-8]|24[0-7]|26[12]~24[46-9]|254|26[23]~7[5-7]~5[4-9]|53[7-9]~6[7-9]|6010|607008|777~3[6-9]~403~16|1440[3-9]~49~1[89]~81[4-9]|82|83[0-5]~6[0-6]|536~5[0-3]~799~396~396~11~79[78]~1[456]~7[0-4]~60[579]~8[0-5]~79[56]~4[5-8]|490~4[0-4]~796~79[34]~3[0-4]~682~737|750~1[23]~17", "zip": "\\d{6}", "zip_name_type": "pin", "zipex": "110034,110001"},
  "data/IO": {"fmt": "%N%n%O%n%A%n%C%n%Z", "id": "data/IO", "key": "IO", "name": "BRITISH INDIAN OCEAN TERRITORY", "require": "ACZ", "upper": "CZ", "zip": "BBND 1ZZ", "zipex": "BBND 1ZZ"},
  "data/IQ": {"fmt": "%O%n%N%n%A%n%C, %S%n%Z", "id": "data/IQ", "key": "IQ", "name": "IRAQ", "require": "ACS", "upper": "CS", "zip": "\\d{5}", "zipex": "31001"},
  "data/IR": {"fmt": "%O%n%N%n%S%n%C, %D%n%A%n%Z", "id": "data/IR", "key": "IR", "lang": "fa", "languages": "fa", "name": "IRAN", "sub_isoids": "01~02~03~04~32~05~06~07~08~29~30~31~10~11~12~13~14~28~26~16~15~17~18~27~19~20~21~22~23~24~25", "sub_keys": "استان آذربایجان شرقی~استان آذربایجان غربی~استان اردبیل~استان اصفهان~استان البرز~استان ایلام~استان بوشهر~استان تهران~استان چهارمحال و بختیاری~استان خراسان جنوبی~استان خراسان رضوی~استان خراسان شمالی~استان خوزستان~استان زنجان~استان سمنان~استان سیستان و بلوچستان~استان فارس~استان قزوین~استان قم~استان کردستان~استان کرمان~استان کرمانشاه~استان کهگیلویه و بویراحمد~استان گلستان~استان گیلان~استان لرستان~استان مازندران~استان مرکزی~استان هرمزگان~استان همدان~استان یزد", "sub_lnames": "East Azerbaijan Province~West Azerbaijan Province~Ardabil Province~Isfahan Province~Alborz Province~Ilam Province~Bushehr Province~Tehran Province~Chaharmahal and Bakhtiari Province~South Khorasan Province~Razavi Khorasan Province~North Khorasan Province~Khuzestan Province~Zanjan Province~Semnan Province~Sistan and Baluchestan Province~Fars Province~Qazvin Province~Qom Province~Kurdistan Province~Kerman Province~Kermanshah Province~Kohgiluyeh and Boyer-Ahmad Province~Golestan Province~Gilan Province~Lorestan Province~Mazandaran Province~Markazi Province~Hormozgan Province~Hamadan Province~Yazd Province", "sublocality_name_type": "neighborhood", "zip": "\\d{5}-?\\d{5}", "zipex": "11936-12345"},
  "data/IS": {"fmt": "%N%n%O%n%A%n%Z %C", "id": "data/IS", "key": "IS", "name": "ICELAND", "posturl": "http://www.postur.is/einstaklingar/posthus/postnumer/", "zip": "\\d{3}", "zipex": "320,121,220,110"},
  "data/IT": {"fmt": "%N%n%O%n%A%n%Z %C %S", "id": "data/IT", "key": "IT", "lang": "it", "languages": "it", "name": "ITALY", "posturl": "http://www.poste.it/online/cercacap/", "require": "ACSZ", "sub_isoids": "AG~AL~AN~AO~AR~AP~AT~AV~BA~BT~BL~BN~BG~BI~BO~BZ~BS~BR~CA~CL~CB~CI~CE~CT~CZ~CH~CO~CS~CR~KR~CN~EN~FM~FE~FI~FG~FC~FR~GE~GO~GR~IM~IS~AQ~SP~LT~LE~LC~LI~LO~LU~MC~MN~MS~MT~VS~ME~MI~MO~MB~NA~NO~NU~OG~OT~OR~PD~PA~PR~PV~PG~PU~PE~PC~PI~PT~PN~PZ~PO~RG~RA~RC~RE~RI~RN~RM~RO~SA~SS~SV~SI~SR~SO~TA~TE~TR~TO~TP~TN~TV~TS~UD~VA~VE~VB~VC~VR~VV~VI~VT", "sub_keys": "AG~AL~AN~AO~AR~AP~AT~AV~BA~BT~BL~BN~BG~BI~BO~BZ~BS~BR~CA~CL~CB~CI~CE~CT~CZ~CH~CO~CS~CR~KR~CN~EN~FM~FE~FI~FG~FC~FR~GE~GO~GR~IM~IS~AQ~SP~LT~LE~LC~LI~LO~LU~MC~MN~MS~MT~VS~ME~MI~MO~MB~NA~NO~NU~OG~OT~OR~PD~PA~PR~PV~PG~PU~PE~PC~PI~PT~PN~PZ~PO~RG~RA~RC~RE~RI~RN~RM~RO~SA~SS~SV~SI~SR~SO~TA~TE~TR~TO~TP~TN~TV~TS~UD~VA~VE~VB~VC~VR~VV~VI~VT", "sub_names": "Agrigento~Alessandria~Ancona~Aosta~Arezzo~Ascoli Piceno~Asti~Avellino~Bari~Barletta-Andria-Trani~Belluno~Benevento~Bergamo~Biella~Bologna~Bolzano~Brescia~Brindisi~Cagliari~Caltanissetta~Campobasso~Carbonia-Iglesias~Caserta~Catania~Catanzaro~Chieti~Como~Cosenza~Cremona~Crotone~Cuneo~Enna~Fermo~Ferrara~Firenze~Foggia~Forlì-Cesena~Frosinone~Genova~Gorizia~Grosseto~Imperia~Isernia~L'Aquila~La Spezia~Latina~Lecce~Lecco~Livorno~Lodi~Lucca~Macerata~Mantova~Massa-Carrara~Matera~Medio Campidano~Messina~Milano~Modena~Monza e Brianza~Napoli~Novara~Nuoro~Ogliastra~Olbia-Tempio~Oristano~Padova~Palermo~Parma~Pavia~Perugia~Pesaro e Urbino~Pescara~Piacenza~Pisa~Pistoia~Pordenone~Potenza~Prato~Ragusa~Ravenna~Reggio Calabria~Reggio Emilia~Rieti~Rimini~Roma~Rovigo~Salerno~Sassari~Savona~Siena~Siracusa~Sondrio~Taranto~Teramo~Terni~Torino~Trapani~Trento~Treviso~Trieste~Udine~Varese~Venezia~Verbano-Cusio-Ossola~Vercelli~Verona~Vibo Valentia~Vicenza~Viterbo", "sub_zips": "92~15~60~11~52~63~14~83~70~76[01]~32~82~24~13[89]~40~39~25~72~0912[1-9]|0913[0-4]|0901[0289]|0902[03468]|0903[0234]|0904|0803[035]|08043~93~860[1-4]|86100~0901[013-7]~81~95~88[01]~66~22~87~26[01]~88[89]~12|18025~94~638|63900~44~50~71~47[015]~03~16~34[01]7~58~18~860[7-9]|86170~67~19~04~73~23[89]~57~26[89]~55~62~46~54~75~0902[012579]|0903[015-9]|09040~98~20~41~208|20900~80~28[01]~080[1-3]|08100~08037|0804[024-9]~08020|0702|0703[08]~090[7-9]|09170|0801[039]|0803[04]~35~90~43~27~06~61~65~29~56~51~330[7-9]|33170~85~59~97~48~89[01]~42~02~47[89]~00~45~84~070[14]|0703[0-79]|07100~17|12071~53~96~23[01]~74~64~05~10~91~38~31~3401|341[0-689]|34062~330[1-5]|33100~21~30~28[89]~13[01]~37~89[89]~36~01", "upper": "CS", "zip": "\\d{5}", "zipex": "00144,47037,39049"},
  "data/JE": {"fmt": "%N%n%O%n%A%n%C%nJERSEY%n%Z", "id": "data/JE", "key": "JE", "name": "CHANNEL ISLANDS", "posturl": "http://www.jerseypost.com/tools/postcode-address-finder/", "require": "ACZ", "upper": "CZ", "zip": "JE\\d[\\dA-Z]? ?\\d[ABD-HJLN-UW-Z]{2}", "zipex": "JE1 1AA,JE2 2BT"},
  "data/JM": {"fmt": "%N%n%O%n%A%n%C%n%S %X", "id": "data/JM", "key": "JM", "lang": "en", "languages": "en", "name": "JAMAICA", "require": "ACS", "state_name_type": "parish", "sub_isoids": "13~09~01~12~04~02~06~14~11~08~05~03~07~10", "sub_keys": "Clarendon~Hanover~Kingston~Manchester~Portland~St. Andrew~St. Ann~St. Catherine~St. Elizabeth~St. James~St. Mary~St. Thomas~Trelawny~Westmoreland"},
  "data/JO": {"fmt": "%N%n%O%n%A%n%C %Z", "id": "data/JO", "key": "JO", "name": "JORDAN", "zip": "\\d{5}", "zipex": "11937,11190"},
  "data/JP": {"fmt": "〒%Z%n%S%n%A%n%O%n%N", "id": "data/JP", "key": "JP", "lang": "ja", "languages": "ja", "lfmt": "%N%n%O%n%A, %S%n%Z", "name": "JAPAN", "posturl": "http://www.post.japanpost.jp/zipcode/", "require": "ASZ", "state_name_type": "prefecture", "sub_isoids": "01~02~03~04~05~06~07~08~09~10~11~12~13~14~15~16~17~18~19~20~21~22~23~24~25~26~27~28~29~30~31~32~33~34~35~36~37~38~39~40~41~42~43~44~45~46~47", "sub_keys": "北海道~青森県~岩手県~宮城県~秋田県~山形県~福島県~茨城県~栃木県~群馬県~埼玉県~千葉県~東京都~神奈川県~新潟県~富山県~石川県~福井県~山梨県~長野県~岐阜県~静岡県~愛知県~三重県~滋賀県~京都府~大阪府~兵庫県~奈良県~和歌山県~鳥取県~島根県~岡山県~広島県~山口県~徳島県~香川県~愛媛県~高知県~福岡県~佐賀県~長崎県~熊本県~大分県~宮崎県~鹿児島県~沖縄県", "sub_lnames": "Hokkaido~Aomori~Iwate~Miyagi~Akita~Yamagata~Fukushima~Ibaraki~Tochigi~Gunma~Saitama~Chiba~Tokyo~Kanagawa~Niigata~Toyama~Ishikawa~Fukui~Yamanashi~Nagano~Gifu~Shizuoka~Aichi~Mie~Shiga~Kyoto~Osaka~Hyogo~Nara~Wakayama~Tottori~Shimane~Okayama~Hiroshima~Yamaguchi~Tokushima~Kagawa~Ehime~Kochi~Fukuoka~Saga~Nagasaki~Kumamoto~Oita~Miyazaki~Kagoshima~Okinawa", "sub_zips": "0[4-9]|00[1-7]~03|018~02~98~01~99~9[67]~3[01]~32|311|349~37|38[49]~3[3-6]~2[6-9]~1[0-8]|19[0-8]|20~2[1-5]|199~9[45]|389~93~92|939~91|922~40~3[89]|949~50~4[1-9]~4[4-9]|431~51|498|647~52~6[0-2]|520~5[3-9]|618|630~6[5-7]|563~63|64[78]~64|519~68~69|68[45]~7[01]~7[23]~7[45]~77~76~79~78~8[0-3]|871~84~85|81[17]|848~86~87|839~88~89~90", "upper": "S", "zip": "\\d{3}-?\\d{4}", "zipex": "154-0023,350-1106,951-8073,112-0001,208-0032,231-0012"},
  "data/KE": {"fmt": "%N%n%O%n%A%n%C%n%Z", "id": "data/KE", "key": "KE", "name": "KENYA", "zip": "\\d{5}", "zipex": "20100,00100"},
  "data/KG": {"fmt": "%N%n%O%n%A%n%Z %C", "id": "data/KG", "key": "KG", "name": "KYRGYZSTAN", "zip": "\\d{6}", "zipex": "720001"},
  "data/KH": {"fmt": "%N%n%O%n%A%n%C %Z", "id": "data/KH", "key": "KH", "name": "CAMBODIA", "zip": "\\d{5}", "zipex": "12203,14206,12000"},
  "data/KI": {"fmt": "%N%n%O%n%A%n%S%n%C", "id": "data/KI", "key": "KI", "name": "KIRIBATI", "state_name_type": "island", "upper": "ACNOS"},
  "data/KM": {"id": "data/KM", "key": "KM", "name": "COMOROS", "upper": "AC"},
  "data/KN": {"fmt": "%N%n%O%n%A%n%C, %S", "id": "data/KN", "key": "KN", "lang": "en", "languages": "en", "name": "SAINT KITTS AND NEVIS", "require": "ACS", "state_name_type": "island", "sub_isoids": "N~K", "sub_keys": "Nevis~St. Kitts"},
  "data/KP": {"fmt": "%Z%n%S%n%C%n%A%n%O%n%N", "id": "data/KP", "key": "KP", "lang": "ko", "languages": "ko", "lfmt": "%N%n%O%n%A%n%C%n%S, %Z", "name": "NORTH KOREA", "sub_isoids": "07~13~10~04~02~03~01~08~09~05~06", "sub_keys": "강원도~라선 특별시~량강도~자강도~평안 남도~평안 북도~평양 직할시~함경 남도~함경 북도~황해남도~황해북도", "sub_lnames": "Kangwon~Rason~Ryanggang~Chagang~South Pyongan~North Pyongan~Pyongyang~South Hamgyong~North Hamgyong~South Hwanghae~North Hwanghae"},
  "data/KR": {"fmt": "%S %C%D%n%A%n%O%n%N%n%Z", "id": "data/KR", "key": "KR", "lang": "ko", "languages": "ko", "lfmt": "%N%n%O%n%A%n%D%n%C%n%S%n%Z", "name": "SOUTH KOREA", "posturl": "http://www.epost.go.kr/search/zipcode/search5.jsp", "require": "ACSZ", "state_name_type": "do_si", "sub_isoids": "42~41~48~47~29~27~30~26~11~50~31~28~46~45~49~44~43", "sub_keys": "강원도~경기도~경상남도~경상북도~광주광역시~대구광역시~대전광역시~부산광역시~서울특별시~세종특별자치시~울산광역시~인천광역시~전라남도~전라북도~제주특별자치도~충청남도~충청북도", "sub_lnames": "Gangwon-do~Gyeonggi-do~Gyeongsangnam-do~Gyeongsangbuk-do~Gwangju~Daegu~Daejeon~Busan~Seoul~Sejong~Ulsan~Incheon~Jeollanam-do~Jeollabuk-do~Jeju-do~Chungcheongnam-do~Chungcheongbuk-do", "sub_mores": "true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true", "sub_names": "강원~경기~경남~경북~광주~대구~대전~부산~서울~세종~울산~인천~전남~전북~제주~충남~충북", "sub_zipexs": "25627~12410~53286~38540~62394~42456~34316~46706~06321~30065~44782~23024~59222~56445~63563~32832~28006", "sub_zips": "2[456]\\d{2}~1[0-8]\\d{2}~5[0-3]\\d{2}~(?:3[6-9]|40)\\d{2}~6[12]\\d{2}~4[12]\\d{2}~3[45]\\d{2}~4[6-9]\\d{2}~0[1-8]\\d{2}~30[01]\\d~4[45]\\d{2}~2[1-3]\\d{2}~5[7-9]\\d{2}~5[4-6]\\d{2}~63[0-356]\\d~3[1-3]\\d{2}~2[789]\\d{2}", "sublocality_name_type": "district", "upper": "Z", "zip": "\\d{5}", "zipex": "03051"},
  "data/KW": {"fmt": "%N%n%O%n%A%n%Z %C", "id": "data/KW", "key": "KW", "name": "KUWAIT", "zip": "\\d{5}", "zipex": "54541,54551,54404,13009"},
  "data/KY": {"fmt": "%N%n%O%n%A%n%S %Z", "id": "data/KY", "key": "KY", "lang": "en", "languages": "en", "name": "CAYMAN ISLANDS", "posturl": "http://www.caymanpost.gov.ky/", "require": "AS", "state_name_type": "island", "sub_keys": "Cayman Brac~Grand Cayman~Little Cayman", "zip": "KY\\d-\\d{4}", "zipex": "KY1-1100,KY1-1702,KY2-2101"},
  "data/KZ": {"fmt": "%Z%n%S%n%C%n%A%n%O%n%N", "id": "data/KZ", "key": "KZ", "name": "KAZAKHSTAN", "zip": "\\d{6}", "zipex": "040900,050012"},
  "data/LA": {"fmt": "%N%n%O%n%A%n%Z %C", "id": "data/LA", "key": "LA", "name": "LAO (PEOPLE'S DEM. REP.)", "zip": "\\d{5}", "zipex": "01160,01000"},
  "data/LB": {"fmt": "%N%n%O%n%A%n%C %Z", "id": "data/LB", "key": "LB", "name": "LEBANON", "zip": "(?:\\d{4})(?: ?(?:\\d{4}))?", "zipex": "2038 3054,1107 2810,1000"},
  "data/LC": {"id": "data/LC", "key": "LC", "name": "SAINT LUCIA"},
  "data/LI": {"fmt": "%O%n%N%n%A%nFL-%Z %C", "id": "data/LI", "key": "LI", "name": "LIECHTENSTEIN", "postprefix": "FL-", "posturl": "http://www.post.ch/db/owa/pv_plz_pack/pr_main", "require": "ACZ", "zip": "948[5-9]|949[0-8]", "zipex": "9496,9491,9490,9485"},
  "data/LK": {"fmt": "%N%n%O%n%A%n%C%n%Z", "id": "data/LK", "key": "LK", "name": "SRI LANKA", "posturl": "http://www.slpost.gov.lk/", "zip": "\\d{5}", "zipex": "20000,00100"},
  "data/LR": {"fmt": "%N%n%O%n%A%n%Z %C", "id": "data/LR", "key": "LR", "name": "LIBERIA", "zip": "\\d{4}", "zipex": "1000"},
  "data/LS": {"fmt": "%N%n%O%n%A%n%C %Z", "id": "data/LS", "key": "LS", "name": "LESOTHO", "zip": "\\d{3}", "zipex": "100"},
  "data/LT": {"fmt": "%O%n%N%n%A%nLT-%Z %C", "id": "data/LT", "key": "LT", "name": "LITHUANIA", "postprefix": "LT-", "posturl": "http://www.post.lt/lt/?id=316", "zip": "\\d{5}", "zipex": "04340,03500"},
  "data/LU": {"fmt": "%O%n%N%n%A%nL-%Z %C", "id": "data/LU", "key": "LU", "name": "LUXEMBOURG", "postprefix": "L-", "posturl": "https://www.post.lu/fr/grandes-entreprises/solutions-postales/rechercher-un-code-postal", "require": "ACZ", "zip": "\\d{4}", "zipex": "4750,2998"},
  "data/LV": {"fmt": "%N%n%O%n%A%n%C, %Z", "id": "data/LV", "key": "LV", "name": "LATVIA", "posturl": "http://www.pasts.lv/lv/uzzinas/nodalas/", "zip": "LV-\\d{4}", "zipex": "LV-1073,LV-1000"},
  "data/LY": {"id": "data/LY", "key": "LY", "name": "LIBYA"},
  "data/MA": {"fmt": "%N%n%O%n%A%n%Z %C", "id": "data/MA", "key": "MA", "name": "MOROCCO", "zip": "\\d{5}", "zipex": "53000,10000,20050,16052"},
  "data/MC": {"fmt": "%N%n%O%n%A%nMC-%Z %C %X", "id": "data/MC", "key": "MC", "name": "MONACO", "postprefix": "MC-", "zip": "980\\d{2}", "zipex": "98000,98020,98011,98001"},
  "data/MD": {"fmt": "%N%n%O%n%A%nMD-%Z %C", "id": "data/MD", "key": "MD", "name": "Rep. MOLDOVA", "postprefix": "MD-", "zip": "\\d{4}", "zipex": "2012,2019"},
  "data/ME": {"fmt": "%N%n%O%n%A%n%Z %C", "id": "data/ME", "key": "ME", "name": "MONTENEGRO", "zip": "8\\d{4}", "zipex": "81257,81258,81217,84314,85366"},
  "data/MF": {"fmt": "%O%n%N%n%A%n%Z %C %X", "id": "data/MF", "key": "MF", "name": "SAINT MARTIN", "posturl": "http://www.laposte.fr/Particulier/Utiliser-nos-outils-pratiques/Outils-et-documents/Trouvez-un-code-postal", "require": "ACZ", "upper": "ACX", "zip": "9[78][01]\\d{2}", "zipex": "97100"},
  "data/MG": {"fmt": "%N%n%O%n%A%n%Z %C", "id": "data/MG", "key": "MG", "name": "MADAGASCAR", "zip": "\\d{3}", "zipex": "501,101"},
  "data/MH": {"fmt": "%N%n%O%n%A%n%C %S %Z", "id": "data/MH", "key": "MH", "name": "MARSHALL ISLANDS", "posturl": "http://zip4.usps.com/zip4/welcome.jsp", "require": "ACSZ", "state_name_type": "state", "upper": "ACNOS", "zip": "(969[67]\\d)(?:[ \\-](\\d{4}))?", "zip_name_type": "zip", "zipex": "96960,96970"},
  "data/MK": {"fmt": "%N%n%O%n%A%n%Z %C", "id": "data/MK", "key": "MK", "name": "MACEDONIA", "zip": "\\d{4}", "zipex": "1314,1321,1443,1062"},
  "data/ML": {"id": "data/ML", "key": "ML", "name": "MALI"},
  "data/MM": {"fmt": "%N%n%O%n%A%n%C, %Z", "id": "data/MM", "key": "MM", "name": "MYANMAR", "zip": "\\d{5}", "zipex": "11181"},
  "data/MN": {"fmt": "%N%n%O%n%A%n%C%n%S %Z", "id": "data/MN", "key": "MN", "name": "MONGOLIA", "posturl": "http://www.zipcode.mn/", "zip": "\\d{5}", "zipex": "65030,65270"},
  "data/MO": {"fmt": "%A%n%O%n%N", "id": "data/MO", "key": "MO", "lfmt": "%N%n%O%n%A", "name": "MACAO", "require": "A"},
  "data/MP": {"fmt": "%N%n%O%n%A%n%C %S %Z", "id": "data/MP", "key": "MP", "name": "NORTHERN MARIANA ISLANDS", "posturl": "http://zip4.usps.com/zip4/welcome.jsp", "require": "ACSZ", "state_name_type": "state", "upper": "ACNOS", "zip": "(9695[012])(?:[ \\-](\\d{4}))?", "zip_name_type": "zip", "zipex": "96950,96951,96952"},
  "data/MQ": {"fmt": "%O%n%N%n%A%n%Z %C %X", "id": "data/MQ", "key": "MQ", "name": "MARTINIQUE", "posturl": "http://www.laposte.fr/Particulier/Utiliser-nos-outils-pratiques/Outils-et-documents/Trouvez-un-code-postal", "require": "ACZ", "upper": "ACX", "zip": "9[78]2\\d{2}", "zipex": "97220"},
  "data/MR": {"id": "data/MR", "key": "MR", "name": "MAURITANIA", "upper": "AC"},
  "data/MS": {"id": "data/MS", "key": "MS", "name": "MONTSERRAT"},
  "data/MT": {"fmt": "%N%n%O%n%A%n%C %Z", "id": "data/MT", "key": "MT", "name": "MALTA", "posturl": "http://postcodes.maltapost.com/", "upper": "CZ", "zip": "[A-Z]{3} ?\\d{2,4}", "zipex": "NXR 01,ZTN 05,GPO 01,BZN 1130,SPB 6031,VCT 1753"},
  "data/MU": {"fmt": "%N%n%O%n%A%n%Z%n%C", "id": "data/MU", "key": "MU", "name": "MAURITIUS", "upper": "CZ", "zip": "\\d{3}(?:\\d{2}|[A-Z]{2}\\d{3})", "zipex": "42602"},
  "data/MV": {"fmt": "%N%n%O%n%A%n%C %Z", "id": "data/MV", "key": "MV", "name": "MALDIVES", "posturl": "http://www.maldivespost.com/?lid=10", "zip": "\\d{5}", "zipex": "20026"},
  "data/MW": {"fmt": "%N%n%O%n%A%n%C %X", "id": "data/MW", "key": "MW", "name": "MALAWI"},
  "data/MX": {"fmt": "%N%n%O%n%A%n%D%n%Z %C, %S", "id": "data/MX", "key": "MX", "lang": "es", "languages": "es", "name": "MEXICO", "posturl": "http://www.correosdemexico.gob.mx/ServiciosLinea/Paginas/ccpostales.aspx", "require": "ACZ", "state_name_type": "state", "sub_isoids": "AGU~BCN~BCS~CAM~CHP~CHH~CMX~COA~COL~DUR~MEX~GUA~GRO~HID~JAL~MIC~MOR~NAY~NLE~OAX~PUE~QUE~ROO~SLP~SIN~SON~TAB~TAM~TLA~VER~YUC~ZAC", "sub_keys": "Ags.~B.C.~B.C.S.~Camp.~Chis.~Chih.~CDMX~Coah.~Col.~Dgo.~Méx.~Gto.~Gro.~Hgo.~Jal.~Mich.~Mor.~Nay.~N.L.~Oax.~Pue.~Qro.~Q.R.~S.L.P.~Sin.~Son.~Tab.~Tamps.~Tlax.~Ver.~Yuc.~Zac.", "sub_names": "Aguascalientes~Baja California~Baja California Sur~Campeche~Chiapas~Chihuahua~Ciudad de México~Coahuila de Zaragoza~Colima~Durango~Estado de México~Guanajuato~Guerrero~Hidalgo~Jalisco~Michoacán~Morelos~Nayarit~Nuevo León~Oaxaca~Puebla~Querétaro~Quintana Roo~San Luis Potosí~Sinaloa~Sonora~Tabasco~Tamaulipas~Tlaxcala~Veracruz~Yucatán~Zacatecas", "sub_zipexs": "20000,20999~21000,22999~23000,23999~24000,24999~29000,30999~31000,33999~00000,16999~25000,27999~28000,28999~34000,35999~50000,57999~36000,38999~39000,41999~42000,43999~44000,49999~58000,61999~62000,62999~63000,63999~64000,67999~68000,71999~72000,75999~76000,76999~77000,77999~78000,79999~80000,82999~83000,85999~86000,86999~87000,89999~90000,90999~91000,96999~97000,97999~98000,99999", "sub_zips": "20~2[12]~23~24~29|30~3[1-3]~0|1[0-6]~2[5-7]~28~3[45]~5[0-7]~3[6-8]~39|4[01]~4[23]~4[4-9]~5[89]|6[01]~62~63~6[4-7]~6[89]|7[01]~7[2-5]~76~77~7[89]~8[0-2]~8[3-5]~86~8[7-9]~90~9[1-6]~97~9[89]", "sublocality_name_type": "neighborhood", "upper": "CSZ", "zip": "\\d{5}", "zipex": "02860,77520,06082"},
  "data/MY": {"fmt": "%N%n%O%n%A%n%D%n%Z %C%n%S", "id": "data/MY", "key": "MY", "lang": "ms", "languages": "ms", "name": "MALAYSIA", "posturl": "http://www.pos.com.my", "require": "ACZ", "state_name_type": "state", "sub_isoids": "01~02~03~14~15~04~05~06~08~09~07~16~12~13~10~11", "sub_keys": "Johor~Kedah~Kelantan~Kuala Lumpur~Labuan~Melaka~Negeri Sembilan~Pahang~Perak~Perlis~Pulau Pinang~Putrajaya~Sabah~Sarawak~Selangor~Terengganu", "sub_zipexs": "79000,86999~05000,09999,34950~15000,18599~50000,60000~87000,87999~75000,78399~70000,73599~25000,28999,39000,49000,69000~30000,36899,39000~01000,02799~10000,14999~62000,62999~88000,91999~93000,98999~40000,48999,63000,68199~20000,24999", "sub_zips": "79|8[0-6]~0[5-9]|34950~1[5-9]~5|60~87~7[5-8]~7[0-4]~2[5-8]|[346]9~3[0-6]|39000~0[12]~1[0-4]~62~8[89]|9[01]~9[3-8]~4[0-8]|6[3-8]~2[0-4]", "sublocality_name_type": "village_township", "upper": "CS", "zip": "\\d{5}", "zipex": "43000,50754,88990,50670"},
  "data/MZ": {"fmt": "%N%n%O%n%A%n%Z %C%S", "id": "data/MZ", "key": "MZ", "lang": "pt", "languages": "pt", "name": "MOZAMBIQUE", "sub_isoids": "P~MPM~G~I~B~L~N~A~S~T~Q", "sub_keys": "Cabo Delgado~Cidade de Maputo~Gaza~Inhambane~Manica~Maputo~Nampula~Niassa~Sofala~Tete~Zambezia", "zip": "\\d{4}", "zipex": "1102,1119,3212"},
  "data/NA": {"id": "data/NA", "key": "NA", "name": "NAMIBIA"},
  "data/NC": {"fmt": "%O%n%N%n%A%n%Z %C %X", "id": "data/NC", "key": "NC", "name": "NEW CALEDONIA", "posturl": "http://poste.opt.nc/index.php?option=com_content&view=article&id=80&Itemid=131", "require": "ACZ", "upper": "ACX", "zip": "988\\d{2}", "zipex": "98814,98800,98810"},
  "data/NE": {"fmt": "%N%n%O%n%A%n%Z %C", "id": "data/NE", "key": "NE", "name": "NIGER", "zip": "\\d{4}", "zipex": "8001"},
  "data/NF": {"fmt": "%O%n%N%n%A%n%C %S %Z", "id": "data/NF", "key": "NF", "name": "NORFOLK ISLAND", "upper": "CS", "zip": "2899", "zipex": "2899"},
  "data/NG": {"fmt": "%N%n%O%n%A%n%D%n%C %Z%n%S", "id": "data/NG", "key": "NG", "lang": "en", "languages": "en", "name": "NIGERIA", "posturl": "http://www.nigeriapostcodes.com/", "state_name_type": "state", "sub_isoids": "AB~AD~AK~AN~BA~BY~BE~BO~CR~DE~EB~ED~EK~EN~FC~GO~IM~JI~KD~KN~KT~KE~KO~KW~LA~NA~NI~OG~ON~OS~OY~PL~RI~SO~TA~YO~ZA", "sub_keys": "Abia~Adamawa~Akwa Ibom~Anambra~Bauchi~Bayelsa~Benue~Borno~Cross River~Delta~Ebonyi~Edo~Ekiti~Enugu~Federal Capital Territory~Gombe~Imo~Jigawa~Kaduna~Kano~Katsina~Kebbi~Kogi~Kwara~Lagos~Nasarawa~Niger~Ogun State~Ondo~Osun~Oyo~Plateau~Rivers~Sokoto~Taraba~Yobe~Zamfara", "upper": "CS", "zip": "\\d{6}", "zipex": "930283,300001,931104"},
  "data/NI": {"fmt": "%N%n%O%n%A%n%Z%n%C, %S", "id": "data/NI", "key": "NI", "lang": "es", "languages": "es", "name": "NICARAGUA", "posturl": "http://www.correos.gob.ni/index.php/codigo-postal-2", "state_name_type": "department", "sub_isoids": "BO~CA~CI~CO~ES~GR~JI~LE~MD~MN~MS~MT~NS~AN~AS~SJ~RI", "sub_keys": "Boaco~Carazo~Chinandega~Chontales~Esteli~Granada~Jinotega~Leon~Madriz~Managua~Masaya~Matagalpa~Nueva Segovia~Raan~Raas~Rio San Juan~Rivas", "sub_zips": "5[12]~4[56]~2[5-7]~5[56]~3[12]~4[34]~6[56]~2[12]~3[45]~1[0-6]~4[12]~6[1-3]~3[7-9]~7[12]~8[1-3]~9[12]~4[78]", "upper": "CS", "zip": "\\d{5}", "zipex": "52000"},
  "data/NL": {"fmt": "%O%n%N%n%A%n%Z %C", "id": "data/NL", "key": "NL", "name": "NETHERLANDS", "posturl": "http://www.postnl.nl/voorthuis/", "require": "ACZ", "zip": "\\d{4} ?[A-Z]{2}", "zipex": "1234 AB,2490 AA"},
  "data/NO": {"fmt": "%N%n%O%n%A%n%Z %C", "id": "data/NO", "key": "NO", "locality_name_type": "post_town", "name": "NORWAY", "posturl": "http://adressesok.posten.no/nb/postal_codes/search", "require": "ACZ", "zip": "\\d{4}", "zipex": "0025,0107,6631"},
  "data/NP": {"fmt": "%N%n%O%n%A%n%C %Z", "id": "data/NP", "key": "NP", "name": "NEPAL", "posturl": "http://www.gpo.gov.np/Home/Postalcode", "zip": "\\d{5}", "zipex": "44601"},
  "data/NR": {"fmt": "%N%n%O%n%A%n%S", "id": "data/NR", "key": "NR", "lang": "en", "languages": "en", "name": "NAURU CENTRAL PACIFIC", "require": "AS", "state_name_type": "district", "sub_isoids": "01~02~03~04~05~06~07~08~09~10~11~12~13~14", "sub_keys": "Aiwo District~Anabar District~Anetan District~Anibare District~Baiti District~Boe District~Buada District~Denigomodu District~Ewa District~Ijuw District~Meneng District~Nibok District~Uaboe District~Yaren District"},
  "data/NU": {"id": "data/NU", "key": "NU", "name": "NIUE"},
  "data/NZ": {"fmt": "%N%n%O%n%A%n%D%n%C %Z", "id": "data/NZ", "key": "NZ", "name": "NEW ZEALAND", "posturl": "http://www.nzpost.co.nz/Cultures/en-NZ/OnlineTools/PostCodeFinder/", "require": "ACZ", "zip": "\\d{4}", "zipex": "6001,6015,6332,8252,1030"},
  "data/OM": {"fmt": "%N%n%O%n%A%n%Z%n%C", "id": "data/OM", "key": "OM", "name": "OMAN", "zip": "(?:PC )?\\d{3}", "zipex": "133,112,111"},
  "data/PA": {"fmt": "%N%n%O%n%A%n%C%n%S", "id": "data/PA", "key": "PA", "name": "PANAMA (REP.)", "upper": "CS"},
  "data/PE": {"fmt": "%N%n%O%n%A%n%C %Z%n%S", "id": "data/PE", "key": "PE", "lang": "es", "languages": "es", "locality_name_type": "district", "name": "PERU", "posturl": "http://www.serpost.com.pe/cpostal/codigo", "sub_isoids": "AMA~ANC~APU~ARE~AYA~CAJ~CAL~CUS~LIM~HUV~HUC~ICA~JUN~LAL~LAM~LOR~MDD~MOQ~LMA~PAS~PIU~PUN~SAM~TAC~TUM~UCA", "sub_keys": "Amazonas~Áncash~Apurímac~Arequipa~Ayacucho~Cajamarca~Callao~Cuzco~Gobierno Regional de Lima~Huancavelica~Huánuco~Ica~Junín~La Libertad~Lambayeque~Loreto~Madre de Dios~Moquegua~Municipalidad Metropolitana de Lima~Pasco~Piura~Puno~San Martín~Tacna~Tumbes~Ucayali", "zip": "(?:LIMA \\d{1,2}|CALLAO 0?\\d)|[0-2]\\d{4}", "zipex": "LIMA 23,LIMA 42,CALLAO 2,02001"},
  "data/PF": {"fmt": "%N%n%O%n%A%n%Z %C %S", "id": "data/PF", "key": "PF", "name": "FRENCH POLYNESIA", "require": "ACSZ", "state_name_type": "island", "upper": "CS", "zip": "987\\d{2}", "zipex": "98709"},
  "data/PG": {"fmt": "%N%n%O%n%A%n%C %Z %S", "id": "data/PG", "key": "PG", "name": "PAPUA NEW GUINEA", "require": "ACS", "zip": "\\d{3}", "zipex": "111"},
  "data/PH": {"fmt": "%N%n%O%n%A%n%D, %C%n%Z %S", "id": "data/PH", "key": "PH", "lang": "en", "languages": "en", "name": "PHILIPPINES", "posturl": "http://www.philpost.gov.ph/", "sub_isoids": "ABR~AGN~AGS~AKL~ALB~ANT~APA~AUR~BAS~BAN~BTN~BTG~BEN~BIL~BOH~BUK~BUL~CAG~CAN~CAS~CAM~CAP~CAT~CAV~CEB~COM~NCO~DAV~DAS~DVO~DAO~DIN~EAS~GUI~IFU~ILN~ILS~ILI~ISA~KAL~LUN~LAG~LAN~LAS~LEY~MAG~MAD~MAS~00~MDC~MDR~MSC~MSR~MOU~NEC~NER~NSA~NUE~NUV~PLW~PAM~PAN~QUE~QUI~RIZ~ROM~WSA~SAR~SIG~SOR~SCO~SLE~SUK~SLU~SUN~SUR~TAR~TAW~ZMB~ZAN~ZAS~ZSI", "sub_keys": "Abra~Agusan del Norte~Agusan del Sur~Aklan~Albay~Antique~Apayao~Aurora~Basilan~Bataan~Batanes~Batangas~Benguet~Biliran~Bohol~Bukidnon~Bulacan~Cagayan~Camarines Norte~Camarines Sur~Camiguin~Capiz~Catanduanes~Cavite~Cebu~Compostela Valley~Cotabato~Davao del Norte~Davao del Sur~Davao Occidental~Davao Oriental~Dinagat Islands~Eastern Samar~Guimaras~Ifugao~Ilocos Norte~Ilocos Sur~Iloilo~Isabela~Kalinga~La Union~Laguna~Lanao del Norte~Lanao del Sur~Leyte~Maguindanao~Marinduque~Masbate~Metro Manila~Mindoro Occidental~Mindoro Oriental~Misamis Occidental~Misamis Oriental~Mountain Province~Negros Occidental~Negros Oriental~Northern Samar~Nueva Ecija~Nueva Vizcaya~Palawan~Pampanga~Pangasinan~Quezon Province~Quirino~Rizal~Romblon~Samar~Sarangani~Siquijor~Sorsogon~South Cotabato~Southern Leyte~Sultan Kudarat~Sulu~Surigao del Norte~Surigao del Sur~Tarlac~Tawi-Tawi~Zambales~Zamboanga del Norte~Zamboanga del Sur~Zamboanga Sibuguey", "sub_zipexs": "2800,2826~8600,8611~8500,8513~5600,5616~4500,4517~5700,5717~3800,3806,3808~3200,3207~7300,7306~2100,2114~3900,3905~4200,4234~2600,2615~6543,6550~6300,6337~8700,8723~3000,3024~3500,3528~4600,4612~4400,4436~9100,9104~5800,5816~4800,4810~4100,4126~6000,6053~8800,8810~9400,9417~8100,8120~8000,8010~8015,8013~8200,8210~8426,8412~6800,6822~5044,5046~3600,3610~2900,2922~2700,2733~5000,5043~3300,3336~3807,3809,3814~2500,2520~4000,4033~9200,9223~9300,9321,9700,9716~6500,6542~9600,9619~4900,4905~5400,5421~~5100,5111~5200,5214~7200,7215~9000,9025~2616,2625~6100,6132~6200,6224~6400,6423~3100,3133~3700,3714~5300,5322~2000,2022~2400,2447~4300,4342~3400,3405~1850,1990~5500,5516~6700,6725~8015~6225,6230~4700,4715~9500,9513~6600,6613~9800,9811~7400,7416~8400,8425~8300,8319~2300,2318~7500,7509~2200,2213~7100,7124~7000,7043~7000,7043", "sub_zips": "28[0-2]~86[01]~85[01]~56[01]~45[01]~57[01]~380[0-68]~320~730~21[01]~390~42[0-3]~26(0|1[0-5])~65(4[3-9]|5)~63[0-3]~87[0-2]~30[0-2]~35[0-2]~46[01]~44[0-3]~910~58[01]~48[01]~41[0-2]~60[0-5]~88[01]~94[01]~81[0-2]~80[01]~801[1-5]~82[01]~84[12]~68[0-2]~504[4-6]~36[01]~29[0-2]~27[0-3]~50([0-3]|4[0-3])~33[0-3]~38(0[79]|1[0-4])~25[0-2]~40[0-3]~92[0-2]~9(3[0-2]|7[01])~65([0-3]|4[0-2])~96[01]~490~54[0-2]~~51[01]~52[01]~72[01]~90[0-2]~26(1[6-9]|2[0-5])~61[0-3]~62[0-2]~64[0-2]~31[0-3]~37[01]~53[0-2]~20[0-2]~24[0-4]~43[0-4]~340~1[89]~55[01]~67[0-2]~8015~62(2[5-9]|30)~47[01]~95[01]~66[10]~98[01]~74[01]~84[0-2]~83[01]~23[01]~750~22[01]~71[0-2]~70[0-4]~70[0-4]", "zip": "\\d{4}", "zipex": "1008,1050,1135,1207,2000,1000"},
  "data/PK": {"fmt": "%N%n%O%n%A%n%C-%Z", "id": "data/PK", "key": "PK", "name": "PAKISTAN", "posturl": "http://www.pakpost.gov.pk/postcode.php", "zip": "\\d{5}", "zipex": "44000"},
  "data/PL": {"fmt": "%N%n%O%n%A%n%Z %C", "id": "data/PL", "key": "PL", "name": "POLAND", "posturl": "http://kody.poczta-polska.pl/", "require": "ACZ", "zip": "\\d{2}-\\d{3}", "zipex": "00-950,05-470,48-300,32-015,00-940"},
  "data/PM": {"fmt": "%O%n%N%n%A%n%Z %C %X", "id": "data/PM", "key": "PM", "name": "ST. PIERRE AND MIQUELON", "require": "ACZ", "upper": "ACX", "zip": "9[78]5\\d{2}", "zipex": "97500"},
  "data/PN": {"fmt": "%N%n%O%n%A%n%C%n%Z", "id": "data/PN", "key": "PN", "name": "PITCAIRN", "require": "ACZ", "upper": "CZ", "zip": "PCRN 1ZZ", "zipex": "PCRN 1ZZ"},
  "data/PR": {"fmt": "%N%n%O%n%A%n%C PR %Z", "id": "data/PR", "key": "PR", "name": "PUERTO RICO", "postprefix": "PR ", "posturl": "http://zip4.usps.com/zip4/welcome.jsp", "require": "ACZ", "upper": "ACNO", "zip": "(00[679]\\d{2})(?:[ \\-](\\d{4}))?", "zip_name_type": "zip", "zipex": "00930"},
  "data/PT": {"fmt": "%N%n%O%n%A%n%Z %C", "id": "data/PT", "key": "PT", "name": "PORTUGAL", "posturl": "http://www.ctt.pt/feapl_2/app/open/tools.jspx?tool=1", "require": "ACZ", "zip": "\\d{4}-\\d{3}", "zipex": "2725-079,1250-096,1201-950,2860-571,1208-148"},
  "data/PW": {"fmt": "%N%n%O%n%A%n%C %S %Z", "id": "data/PW", "key": "PW", "name": "PALAU", "posturl": "http://zip4.usps.com/zip4/welcome.jsp", "require": "ACSZ", "state_name_type": "state", "upper": "ACNOS", "zip": "(969(?:39|40))(?:[ \\-](\\d{4}))?", "zip_name_type": "zip", "zipex": "96940"},
  "data/PY": {"fmt": "%N%n%O%n%A%n%Z %C", "id": "data/PY", "key": "PY", "name": "PARAGUAY", "zip": "\\d{4}", "zipex": "1536,1538,1209"},
  "data/QA": {"id": "data/QA", "key": "QA", "name": "QATAR", "upper": "AC"},
  "data/RE": {"fmt": "%O%n%N%n%A%n%Z %C %X", "id": "data/RE", "key": "RE", "name": "REUNION", "posturl": "http://www.laposte.fr/Particulier/Utiliser-nos-outils-pratiques/Outils-et-documents/Trouvez-un-code-postal", "require": "ACZ", "upper": "ACX", "zip": "9[78]4\\d{2}", "zipex": "97400"},
  "data/RO": {"fmt": "%N%n%O%n%A%n%Z %C", "id": "data/RO", "key": "RO", "name": "ROMANIA", "posturl": "http://www.posta-romana.ro/zip_codes", "upper": "AC", "zip": "\\d{6}", "zipex": "060274,061357,200716"},
  "data/RS": {"fmt": "%N%n%O%n%A%n%Z %C", "id": "data/RS", "key": "RS", "name": "REPUBLIC OF SERBIA", "posturl": "http://www.posta.rs/struktura/lat/aplikacije/pronadji/nadji-postu.asp", "zip": "\\d{5,6}", "zipex": "106314"},
  "data/RU": {"fmt": "%N%n%O%n%A%n%C%n%S%n%Z", "id": "data/RU", "key": "RU", "lang": "ru", "languages": "ru", "lfmt": "%N%n%O%n%A%n%C%n%S%n%Z", "name": "RUSSIAN FEDERATION", "posturl": "http://info.russianpost.ru/servlet/department", "require": "ACSZ", "state_name_type": "oblast", "sub_isoids": "ALT~AMU~ARK~AST~BEL~BRY~VLA~VGG~VLG~VOR~YEV~ZAB~IVA~IRK~KB~KGD~KLU~KAM~KC~KEM~KIR~KOS~KDA~KYA~KGN~KRS~LEN~LIP~MAG~MOW~MOS~MUR~NEN~NIZ~NGR~NVS~OMS~ORE~ORL~PNZ~PER~PRI~PSK~AD~AL~BA~BU~DA~IN~KL~KR~KO~~ME~MO~SA~SE~TA~TY~UD~KK~ROS~RYA~SAM~SPE~SAR~SAK~SVE~~SMO~STA~TAM~TVE~TOM~TUL~TYU~ULY~KHA~KHM~CHE~CE~CU~CHU~YAN~YAR", "sub_keys": "Алтайский край~Амурская область~Архангельская область~Астраханская область~Белгородская область~Брянская область~Владимирская область~Волгоградская область~Вологодская область~Воронежская область~Еврейская автономная область~Забайкальский край~Ивановская область~Иркутская область~Кабардино-Балкарская Республика~Калининградская область~Калужская область~Камчатский край~Карачаево-Черкесская Республика~Кемеровская область~Кировская область~Костромская область~Краснодарский край~Красноярский край~Курганская область~Курская область~Ленинградская область~Липецкая область~Магаданская область~Москва~Московская область~Мурманская область~Ненецкий автономный округ~Нижегородская область~Новгородская область~Новосибирская область~Омская область~Оренбургская область~Орловская область~Пензенская область~Пермский край~Приморский край~Псковская область~Республика Адыгея~Республика Алтай~Республика Башкортостан~Республика Бурятия~Республика Дагестан~Республика Ингушетия~Республика Калмыкия~Республика Карелия~Республика Коми~Автономна Республіка Крим~Республика Марий Эл~Республика Мордовия~Республика Саха (Якутия)~Республика Северная Осетия-Алания~Республика Татарстан~Республика Тыва~Республика Удмуртия~Республика Хакасия~Ростовская область~Рязанская область~Самарская область~Санкт-Петербург~Саратовская область~Сахалинская область~Свердловская область~Севастополь~Смоленская область~Ставропольский край~Тамбовская область~Тверская область~Томская область~Тульская область~Тюменская область~Ульяновская область~Хабаровский край~Ханты-Мансийский автономный округ~Челябинская область~Чеченская Республика~Чувашская Республика~Чукотский автономный округ~Ямало-Ненецкий автономный округ~Ярославская область", "sub_lnames": "Altayskiy kray~Amurskaya oblast'~Arkhangelskaya oblast'~Astrakhanskaya oblast'~Belgorodskaya oblast'~Bryanskaya oblast'~Vladimirskaya oblast'~Volgogradskaya oblast'~Vologodskaya oblast'~Voronezhskaya oblast'~Evreyskaya avtonomnaya oblast'~Zabaykalskiy kray~Ivanovskaya oblast'~Irkutskaya oblast'~Kabardino-Balkarskaya Republits~Kaliningradskaya oblast'~Kaluzhskaya oblast'~Kamchatskiy kray~Karachaevo-Cherkesskaya Republits~Kemerovskaya oblast'~Kirovskaya oblast'~Kostromskaya oblast'~Krasnodarskiy kray~Krasnoyarskiy kray~Kurganskaya oblast'~Kurskaya oblast'~Leningradskaya oblast'~Lipetskaya oblast'~Magadanskaya oblast'~Moskva~Moskovskaya oblast'~Murmanskaya oblast'~Nenetskiy~Nizhegorodskaya oblast'~Novgorodskaya oblast'~Novosibirskaya oblast'~Omskaya oblast'~Orenburgskaya oblast'~Orlovskaya oblast'~Penzenskaya oblast'~Permskiy kray~Primorskiy kray~Pskovskaya oblast'~Respublika Adygeya~Altay Republits~Bashkortostan Republits~Buryatiya Republits~Dagestan Republits~Ingushetiya Republits~Respublika Kalmykiya~Kareliya Republits~Komi Republits~Respublika Krym~Respublika Mariy El~Respublika Mordoviya~Sakha (Yakutiya) Republits~Respublika Severnaya Osetiya-Alaniya~Respublika Tatarstan~Tyva Republits~Respublika Udmurtiya~Khakasiya Republits~Rostovskaya oblast'~Ryazanskaya oblast'~Samarskaya oblast'~Sankt-Peterburg~Saratovskaya oblast'~Sakhalinskaya oblast'~Sverdlovskaya oblast'~Sevastopol'~Smolenskaya oblast'~Stavropolskiy kray~Tambovskaya oblast'~Tverskaya oblast'~Tomskaya oblast'~Tulskaya oblast'~Tyumenskaya oblast'~Ulyanovskaya oblast'~Khabarovskiy kray~Khanty-Mansiyskiy avtonomnyy okrug~Chelyabinskaya oblast'~Chechenskaya Republits~Chuvashia~Chukotskiy~Yamalo-Nenetskiy~Yaroslavskaya oblast'", "sub_names": "Алтайский край~Амурская область~Архангельская область~Астраханская область~Белгородская область~Брянская область~Владимирская область~Волгоградская область~Вологодская область~Воронежская область~Еврейская автономная область~Забайкальский край~Ивановская область~Иркутская область~Кабардино-Балкарская Республика~Калининградская область~Калужская область~Камчатский край~Карачаево-Черкесская Республика~Кемеровская область~Кировская область~Костромская область~Краснодарский край~Красноярский край~Курганская область~Курская область~Ленинградская область~Липецкая область~Магаданская область~Москва~Московская область~Мурманская область~Ненецкий автономный округ~Нижегородская область~Новгородская область~Новосибирская область~Омская область~Оренбургская область~Орловская область~Пензенская область~Пермский край~Приморский край~Псковская область~Республика Адыгея~Республика Алтай~Республика Башкортостан~Республика Бурятия~Республика Дагестан~Республика Ингушетия~Республика Калмыкия~Республика Карелия~Республика Коми~Республика Крым~Республика Марий Эл~Республика Мордовия~Республика Саха (Якутия)~Республика Северная Осетия-Алания~Республика Татарстан~Республика Тыва~Республика Удмуртия~Республика Хакасия~Ростовская область~Рязанская область~Самарская область~Санкт-Петербург~Саратовская область~Сахалинская область~Свердловская область~Севастополь~Смоленская область~Ставропольский край~Тамбовская область~Тверская область~Томская область~Тульская область~Тюменская область~Ульяновская область~Хабаровский край~Ханты-Мансийский автономный округ~Челябинская область~Чеченская Республика~Чувашская Республика~Чукотский автономный округ~Ямало-Ненецкий автономный округ~Ярославская область", "sub_zips": "65[6-9]~67[56]~16[3-5]~41[4-6]~30[89]~24[1-3]~60[0-2]~40[0-4]~16[0-2]~39[4-7]~679~6(?:7[2-4]|87)~15[3-5]~66[4-9]~36[01]~23[6-8]~24[89]~68[348]~369~65[0-4]~61[0-3]~15[67]~35[0-4]~6(?:6[0-3]|4[78])~64[01]~30[5-7]~18[78]~39[89]~68[56]~1(?:0[1-9]|1|2|3[0-5]|4[0-4])~14[0-4]~18[34]~166~60[3-7]~17[3-5]~63[0-3]~64[4-6]~46[0-2]~30[23]~44[0-2]~61[4-9]~69[0-2]~18[0-2]~385~649~45[0-3]~67[01]~36[78]~386~35[89]~18[56]~16[7-9]~29[5-8]~42[45]~43[01]~67[78]~36[23]~42[0-3]~66[78]~42[67]~655~34[4-7]~39[01]~44[3-6]~19~41[0-3]~69[34]~62[0-4]~299~21[4-6]~35[5-7]~39[23]~17[0-2]~63[4-6]~30[01]~62[5-7]~43[23]~68[0-2]~628~45[4-7]~36[4-6]~42[89]~689~629~15[0-2]", "upper": "AC", "zip": "\\d{6}", "zipex": "247112,103375,188300"},
  "data/RW": {"id": "data/RW", "key": "RW", "name": "RWANDA", "upper": "AC"},
  "data/SA": {"fmt": "%N%n%O%n%A%n%C %Z", "id": "data/SA", "key": "SA", "name": "SAUDI ARABIA", "zip": "\\d{5}", "zipex": "11564,11187,11142"},
  "data/SB": {"id": "data/SB", "key": "SB", "name": "SOLOMON ISLANDS"},
  "data/SC": {"fmt": "%N%n%O%n%A%n%C%n%S", "id": "data/SC", "key": "SC", "name": "SEYCHELLES", "state_name_type": "island", "upper": "S"},
  "data/SD": {"fmt": "%N%n%O%n%A%n%C%n%Z", "id": "data/SD", "key": "SD", "locality_name_type": "district", "name": "SUDAN", "zip": "\\d{5}", "zipex": "11042,11113"},
  "data/SE": {"fmt": "%O%n%N%n%A%nSE-%Z %C", "id": "data/SE", "key": "SE", "locality_name_type": "post_town", "name": "SWEDEN", "postprefix": "SE-", "posturl": "http://www.posten.se/sv/Kundservice/Sidor/Sok-postnummer-resultat.aspx", "require": "ACZ", "zip": "\\d{3} ?\\d{2}", "zipex": "11455,12345,10500"},
  "data/SG": {"fmt": "%N%n%O%n%A%nSINGAPORE %Z", "id": "data/SG", "key": "SG", "name": "REP. OF SINGAPORE", "posturl": "https://www.singpost.com/find-postal-code", "require": "AZ", "zip": "\\d{6}", "zipex": "546080,308125,408600"},
  "data/SH": {"fmt": "%N%n%O%n%A%n%C%n%Z", "id": "data/SH", "key": "SH", "name": "SAINT HELENA", "require": "ACZ", "upper": "CZ", "zip": "(?:ASCN|STHL) 1ZZ", "zipex": "STHL 1ZZ"},
  "data/SI": {"fmt": "%N%n%O%n%A%nSI-%Z %C", "id": "data/SI", "key": "SI", "name": "SLOVENIA", "postprefix": "SI-", "zip": "\\d{4}", "zipex": "4000,1001,2500"},
  "data/SK": {"fmt": "%N%n%O%n%A%n%Z %C", "id": "data/SK", "key": "SK", "name": "SLOVAKIA", "posturl": "http://psc.posta.sk", "require": "ACZ", "zip": "\\d{3} ?\\d{2}", "zipex": "010 01,023 14,972 48,921 01,975 99"},
  "data/SL": {"id": "data/SL", "key": "SL", "name": "SIERRA LEONE"},
  "data/SM": {"fmt": "%N%n%O%n%A%n%Z %C", "id": "data/SM", "key": "SM", "name": "SAN MARINO", "posturl": "http://www.poste.it/online/cercacap/", "require": "AZ", "zip": "4789\\d", "zipex": "47890,47891,47895,47899"},
  "data/SN": {"fmt": "%N%n%O%n%A%n%Z %C", "id": "data/SN", "key": "SN", "name": "SENEGAL", "zip": "\\d{5}", "zipex": "12500,46024,16556,10000"},
  "data/SO": {"fmt": "%N%n%O%n%A%n%C, %S %Z", "id": "data/SO", "key": "SO", "lang": "so", "languages": "so", "name": "SOMALIA", "require": "ACS", "sub_isoids": "AW~BK~BN~BR~BY~GA~GE~HI~JD~JH~MU~NU~SA~SD~SH~SO~TO~WO", "sub_keys": "AD~BK~BN~BR~BY~GG~GD~HR~JD~JH~MD~NG~SG~SD~SH~SL~TG~WG", "sub_names": "Awdal~Bakool~Banaadir~Bari~Bay~Galguduud~Gedo~Hiiraan~Jubbada Dhexe~Jubbada Hoose~Mudug~Nugaal~Sanaag~Shabeellaha Dhexe~Shabeellaha Hoose~Sool~Togdheer~Woqooyi Galbeed", "upper": "ACS", "zip": "[A-Z]{2} ?\\d{5}", "zipex": "JH 09010,AD 11010"},
  "data/SR": {"fmt": "%N%n%O%n%A%n%C%n%S", "id": "data/SR", "key": "SR", "lang": "nl", "languages": "nl", "name": "SURINAME", "sub_isoids": "BR~CM~CR~MA~NI~PR~PM~SA~SI~WA", "sub_keys": "Brokopondo~Commewijne~Coronie~Marowijne~Nickerie~Para~Paramaribo~Saramacca~Sipaliwini~Wanica", "upper": "AS"},
  "data/SS": {"id": "data/SS", "key": "SS", "name": "SOUTH SUDAN"},
  "data/ST": {"id": "data/ST", "key": "ST", "name": "SAO TOME AND PRINCIPE"},
  "data/SV": {"fmt": "%N%n%O%n%A%n%Z-%C%n%S", "id": "data/SV", "key": "SV", "lang": "es", "languages": "es", "name": "EL SALVADOR", "require": "ACS", "sub_isoids": "AH~CA~CH~CU~LI~PA~UN~MO~SM~SS~SV~SA~SO~US", "sub_keys": "Ahuachapan~Cabanas~Calatenango~Cuscatlan~La Libertad~La Paz~La Union~Morazan~San Miguel~San Salvador~San Vicente~Santa Ana~Sonsonate~Usulutan", "sub_names": "Ahuachapán~Cabañas~Chalatenango~Cuscatlán~La Libertad~La Paz~La Unión~Morazán~San Miguel~San Salvador~San Vicente~Santa Ana~Sonsonate~Usulután", "sub_zipexs": "CP 2101~CP 1201~CP 1301~CP 1401~CP 1501~CP 1601~CP 3101~CP 3201~CP 3301~CP 1101~CP 1701~CP 2201~CP 2301~CP 3401", "sub_zips": "CP 21~CP 12~CP 13~CP 14~CP 15~CP 16~CP 31~CP 32~CP 33~CP 11~CP 17~CP 22~CP 23~CP 34", "upper": "CSZ", "zip": "CP [1-3][1-7][0-2]\\d", "zipex": "CP 1101"},
  "data/SX": {"id": "data/SX", "key": "SX", "name": "SINT MAARTEN"},
  "data/SY": {"id": "data/SY", "key": "SY", "locality_name_type": "district", "name": "SYRIA"},
  "data/SZ": {"fmt": "%N%n%O%n%A%n%C%n%Z", "id": "data/SZ", "key": "SZ", "name": "SWAZILAND", "posturl": "http://www.sptc.co.sz/swazipost/codes/index.php", "upper": "ACZ", "zip": "[HLMS]\\d{3}", "zipex": "H100"},
  "data/TC": {"fmt": "%N%n%O%n%A%n%C%n%Z", "id": "data/TC", "key": "TC", "name": "TURKS AND CAICOS ISLANDS", "require": "ACZ", "upper": "CZ", "zip": "TKCA 1ZZ", "zipex": "TKCA 1ZZ"},
  "data/TD": {"id": "data/TD", "key": "TD", "name": "CHAD"},
  "data/TF": {"id": "data/TF", "key": "TF", "name": "FRENCH SOUTHERN TERRITORIES"},
  "data/TG": {"id": "data/TG", "key": "TG", "name": "TOGO"},
  "data/TH": {"fmt": "%N%n%O%n%A%n%D %C%n%S %Z", "id": "data/TH", "key": "TH", "lang": "th", "languages": "th", "lfmt": "%N%n%O%n%A%n%D, %C%n%S %Z", "name": "THAILAND", "sub_isoids": "81~10~71~46~62~40~38~22~24~20~18~36~86~57~50~92~23~63~26~73~48~30~80~60~12~96~55~31~13~77~25~94~14~56~82~93~66~65~76~67~54~83~44~49~58~35~95~45~85~21~70~16~52~51~42~33~47~90~91~11~75~74~27~19~17~64~72~84~32~43~39~15~37~41~53~61~34", "sub_keys": "กระบี่~กรุงเทพมหานคร~กาญจนบุรี~กาฬสินธุ์~กำแพงเพชร~ขอนแก่น~จังหวัด บึงกาฬ~จันทบุรี~ฉะเชิงเทรา~ชลบุรี~ชัยนาท~ชัยภูมิ~ชุมพร~เชียงราย~เชียงใหม่~ตรัง~ตราด~ตาก~นครนายก~นครปฐม~นครพนม~นครราชสีมา~นครศรีธรรมราช~นครสวรรค์~นนทบุรี~นราธิวาส~น่าน~บุรีรัมย์~ปทุมธานี~ประจวบคีรีขันธ์~ปราจีนบุรี~ปัตตานี~พระนครศรีอยุธยา~พะเยา~พังงา~พัทลุง~พิจิตร~พิษณุโลก~เพชรบุรี~เพชรบูรณ์~แพร่~ภูเก็ต~มหาสารคาม~มุกดาหาร~แม่ฮ่องสอน~ยโสธร~ยะลา~ร้อยเอ็ด~ระนอง~ระยอง~ราชบุรี~ลพบุรี~ลำปาง~ลำพูน~เลย~ศรีสะเกษ~สกลนคร~สงขลา~สตูล~สมุทรปราการ~สมุทรสงคราม~สมุทรสาคร~สระแก้ว~สระบุรี~สิงห์บุรี~สุโขทัย~สุพรรณบุรี~สุราษฎร์ธานี~สุรินทร์~หนองคาย~หนองบัวลำภู~อ่างทอง~อำนาจเจริญ~อุดรธานี~อุตรดิตถ์~อุทัยธานี~อุบลราชธานี", "sub_lnames": "Krabi~Bangkok~Kanchanaburi~Kalasin~Kamphaeng Phet~Khon Kaen~Bueng Kan~Chanthaburi~Chachoengsao~Chon Buri~Chai Nat~Chaiyaphum~Chumpon~Chiang Rai~Chiang Mai~Trang~Trat~Tak~Nakhon Nayok~Nakhon Pathom~Nakhon Phanom~Nakhon Ratchasima~Nakhon Si Thammarat~Nakhon Sawan~Nonthaburi~Narathiwat~Nan~Buri Ram~Pathum Thani~Prachuap Khiri Khan~Prachin Buri~Pattani~Phra Nakhon Si Ayutthaya~Phayao~Phang Nga~Phattalung~Phichit~Phitsanulok~Phetchaburi~Phetchabun~Phrae~Phuket~Maha Sarakham~Mukdahan~Mae Hong Son~Yasothon~Yala~Roi Et~Ranong~Rayong~Ratchaburi~Lop Buri~Lampang~Lamphun~Loei~Si Sa Ket~Sakon Nakhon~Songkhla~Satun~Samut Prakan~Samut Songkhram~Samut Sakhon~Sa Kaeo~Saraburi~Sing Buri~Sukhothai~Suphanburi~Surat Thani~Surin~Nong Khai~Nong Bua Lam Phu~Ang Thong~Amnat Charoen~Udon Thani~Uttaradit~Uthai Thani~Ubon Ratchathani", "sub_zips": "81~10~71~46~62~40~~22~24~20~17~36~86~57~50~92~23~63~26~73~48~30~80~60~11~96~55~31~12~77~25~94~13~56~82~93~66~65~76~67~54~83~44~49~58~35~95~45~85~21~70~15~52~51~42~33~47~90~91~10~75~74~27~18~16~64~72~84~32~43~39~14~37~41~53~61~34", "upper": "S", "zip": "\\d{5}", "zipex": "10150,10210"},
  "data/TJ": {"fmt": "%N%n%O%n%A%n%Z %C", "id": "data/TJ", "key": "TJ", "name": "TAJIKISTAN", "zip": "\\d{6}", "zipex": "735450,734025"},
  "data/TK": {"id": "data/TK", "key": "TK", "name": "TOKELAU"},
  "data/TL": {"id": "data/TL", "key": "TL", "name": "TIMOR-LESTE"},
  "data/TM": {"fmt": "%N%n%O%n%A%n%Z %C", "id": "data/TM", "key": "TM", "name": "TURKMENISTAN", "zip": "\\d{6}", "zipex": "744000"},
  "data/TN": {"fmt": "%N%n%O%n%A%n%Z %C", "id": "data/TN", "key": "TN", "name": "TUNISIA", "posturl": "http://www.poste.tn/codes.php", "zip": "\\d{4}", "zipex": "1002,8129,3100,1030"},
  "data/TO": {"id": "data/TO", "key": "TO", "name": "TONGA"},
  "data/TR": {"fmt": "%N%n%O%n%A%n%Z %C/%S", "id": "data/TR", "key": "TR", "lang": "tr", "languages": "tr", "locality_name_type": "district", "name": "TURKEY", "posturl": "http://postakodu.ptt.gov.tr/", "require": "ACZ", "sub_isoids": "01~02~03~04~68~05~06~07~75~08~09~10~74~72~69~11~12~13~14~15~16~17~18~19~20~21~81~22~23~24~25~26~27~28~29~30~31~76~32~34~35~46~78~70~36~37~38~71~39~40~79~41~42~43~44~45~47~33~48~49~50~51~52~80~53~54~55~56~57~58~63~73~59~60~61~62~64~65~77~66~67", "sub_keys": "Adana~Adıyaman~Afyon~Ağrı~Aksaray~Amasya~Ankara~Antalya~Ardahan~Artvin~Aydın~Balıkesir~Bartın~Batman~Bayburt~Bilecik~Bingöl~Bitlis~Bolu~Burdur~Bursa~Çanakkale~Çankırı~Çorum~Denizli~Diyarbakır~Düzce~Edirne~Elazığ~Erzincan~Erzurum~Eskişehir~Gaziantep~Giresun~Gümüşhane~Hakkari~Hatay~Iğdır~Isparta~İstanbul~İzmir~Kahramanmaraş~Karabük~Karaman~Kars~Kastamonu~Kayseri~Kırıkkale~Kırklareli~Kırşehir~Kilis~Kocaeli~Konya~Kütahya~Malatya~Manisa~Mardin~Mersin~Muğla~Muş~Nevşehir~Niğde~Ordu~Osmaniye~Rize~Sakarya~Samsun~Siirt~Sinop~Sivas~Şanlıurfa~Şırnak~Tekirdağ~Tokat~Trabzon~Tunceli~Uşak~Van~Yalova~Yozgat~Zonguldak", "sub_zips": "01~02~03~04~68~05~06~07~75~08~09~10~74~72~69~11~12~13~14~15~16~17~18~19~20~21~81~22~23~24~25~26~27~28~29~30~31~76~32~34~35~46~78~70~36~37~38~71~39~40~79~41~42~43~44~45~47~33~48~49~50~51~52~80~53~54~55~56~57~58~63~73~59~60~61~62~64~65~77~66~67", "zip": "\\d{5}", "zipex": "01960,06101"},
  "data/TT": {"id": "data/TT", "key": "TT", "name": "TRINIDAD AND TOBAGO"},
  "data/TV": {"fmt": "%N%n%O%n%A%n%C%n%S", "id": "data/TV", "key": "TV", "lang": "tyv", "languages": "tyv", "name": "TUVALU", "state_name_type": "island", "sub_isoids": "FUN~NMG~NMA~~NIT~NUI~NKF~NKL~VAI", "sub_keys": "Funafuti~Nanumanga~Nanumea~Niulakita~Niutao~Nui~Nukufetau~Nukulaelae~Vaitupu", "upper": "ACS"},
  "data/TW": {"fmt": "%Z%n%S%C%n%A%n%O%n%N", "id": "data/TW", "key": "TW", "lang": "zh-Hant", "languages": "zh-Hant", "lfmt": "%N%n%O%n%A%n%C, %S %Z", "name": "TAIWAN", "posturl": "http://www.post.gov.tw/post/internet/f_searchzone/index.jsp?ID=190102", "require": "ACSZ", "state_name_type": "county", "sub_isoids": "TXG~TPE~TTT~TNN~ILA~HUA~~NAN~PIF~MIA~TAO~KHH~KEE~~YUN~NWT~HSZ~HSQ~CYI~CYQ~CHA~PEN", "sub_keys": "台中市~台北市~台東縣~台南市~宜蘭縣~花蓮縣~金門縣~南投縣~屏東縣~苗栗縣~桃園市~高雄市~基隆市~連江縣~雲林縣~新北市~新竹市~新竹縣~嘉義市~嘉義縣~彰化縣~澎湖縣", "sub_lnames": "Taichung City~Taipei City~Taitung County~Tainan City~Yilan County~Hualien County~Kinmen County~Nantou County~Pingtung County~Miaoli County~Taoyuan City~Kaohsiung City~Keelung City~Lienchiang County~Yunlin County~New Taipei City~Hsinchu City~Hsinchu County~Chiayi City~Chiayi County~Changhua County~Penghu County", "sub_mores": "true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true~true", "sub_zipexs": "400,408,411,439~100,119~950,966~700,745~260,272~970,983~890,896~540,558~900,947~350,369~320,338~800,815,817,852~200,206~209,212~630,655~207,208,220,253~~302,315~~602,625~500,530~880,885", "sub_zips": "4[0-3]~1[01]~9[56]~7[0-4]~2[67]~9[78]~89~5[45]~9[0-4]~3[56]~3[23]~8[02-5]|81[1-579]~20[0-6]~209|21[012]~6[3-5]~20[78]|2[2345]~300~30[2-8]|31~600~60[1-9]|6[12]~5[0123]~88", "zip": "\\d{3}(?:\\d{2})?", "zipex": "104,106,10603,40867"},
  "data/TZ": {"fmt": "%N%n%O%n%A%n%Z %C", "id": "data/TZ", "key": "TZ", "name": "TANZANIA (UNITED REP.)", "zip": "\\d{4,5}", "zipex": "6090,34413"},
  "data/UA": {"fmt": "%N%n%O%n%A%n%C%n%S%n%Z", "id": "data/UA", "key": "UA", "lang": "uk", "languages": "uk", "lfmt": "%N%n%O%n%A%n%C%n%S%n%Z", "name": "UKRAINE", "posturl": "http://services.ukrposhta.com/postindex_new/", "require": "ACSZ", "state_name_type": "oblast", "sub_isoids": "43~05~07~12~14~18~21~23~26~30~32~35~09~46~48~51~53~56~40~59~61~63~65~68~71~77~74", "sub_keys": "Автономна Республіка Крим~Вінницька область~Волинська область~Дніпропетровська область~Донецька область~Житомирська область~Закарпатська область~Запорізька область~Івано-Франківська область~місто Київ~Київська область~Кіровоградська область~Луганська область~Львівська область~Миколаївська область~Одеська область~Полтавська область~Рівненська область~місто Севастополь~Сумська область~Тернопільська область~Харківська область~Херсонська область~Хмельницька область~Черкаська область~Чернівецька область~Чернігівська область", "sub_lnames": "Crimea~Vinnyts'ka oblast~Volyns'ka oblast~Dnipropetrovsk oblast~Donetsk oblast~Zhytomyrs'ka oblast~Zakarpats'ka oblast~Zaporiz'ka oblast~Ivano-Frankivs'ka oblast~Kyiv city~Kiev oblast~Kirovohrads'ka oblast~Luhans'ka oblast~Lviv oblast~Mykolaivs'ka oblast~Odessa oblast~Poltavs'ka oblast~Rivnens'ka oblast~Sevastopol' city~Sums'ka oblast~Ternopil's'ka oblast~Kharkiv oblast~Khersons'ka oblast~Khmel'nyts'ka oblast~Cherkas'ka oblast~Chernivets'ka oblast~Chernihivs'ka oblast", "sub_names": "Автономна Республіка Крим~Вінницька область~Волинська область~Дніпропетровська область~Донецька область~Житомирська область~Закарпатська область~Запорізька область~Івано-Франківська область~Київ~Київська область~Кіровоградська область~Луганська область~Львівська область~Миколаївська область~Одеська область~Полтавська область~Рівненська область~Севастополь~Сумська область~Тернопільська область~Харківська область~Херсонська область~Хмельницька область~Черкаська область~Чернівецька область~Чернігівська область", "sub_zips": "9[5-8]~2[1-4]~4[3-5]~49|5[0-3]~8[3-7]~1[0-3]~8[89]|90~69|7[0-2]~7[6-8]~0[1-6]~0[7-9]~2[5-8]~9[1-4]~79|8[0-2]~5[4-7]~6[5-8]~3[6-9]~3[3-5]~99~4[0-2]~4[6-8]~6[1-4]~7[3-5]~29|3[0-2]~1[89]|20~5[89]|60~1[4-7]", "zip": "\\d{5}", "zipex": "15432,01055,01001"},
  "data/UG": {"id": "data/UG", "key": "UG", "name": "UGANDA"},
  "data/US": {"fmt": "%N%n%O%n%A%n%C, %S %Z", "id": "data/US", "key": "US", "lang": "en", "languages": "en", "name": "UNITED STATES", "posturl": "https://tools.usps.com/go/ZipLookupAction!input.action", "require": "ACSZ", "state_name_type": "state", "sub_isoids": "AL~AK~~AZ~AR~~~~CA~CO~CT~DE~DC~FL~GA~~HI~ID~IL~IN~IA~KS~KY~LA~ME~~MD~MA~MI~~MN~MS~MO~MT~NE~NV~NH~NJ~NM~NY~NC~ND~~OH~OK~OR~~PA~~RI~SC~SD~TN~TX~UT~VT~~VA~WA~WV~WI~WY", "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", "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_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", "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", "upper": "CS", "zip": "(\\d{5})(?:[ \\-](\\d{4}))?", "zip_name_type": "zip", "zipex": "95014,22162-1010"},
  "data/UY": {"fmt": "%N%n%O%n%A%n%Z %C %S", "id": "data/UY", "key": "UY", "lang": "es", "languages": "es", "name": "URUGUAY", "posturl": "http://www.correo.com.uy/index.asp?codPag=codPost&switchMapa=codPost", "sub_isoids": "AR~CA~CL~CO~DU~FS~FD~LA~MA~MO~PA~RN~RV~RO~SA~SJ~SO~TA~TT", "sub_keys": "Artigas~Canelones~Cerro Largo~Colonia~Durazno~Flores~Florida~Lavalleja~Maldonado~Montevideo~Paysandú~Río Negro~Rivera~Rocha~Salto~San José~Soriano~Tacuarembó~Treinta y Tres", "sub_zips": "55~9[01]|1[456]~37~70|75204~97~85~94|9060|97005~30~20~1|91600~60~65|60002~40~27~50~80~75|70003~45~33|30203|30204|30302|37007", "upper": "CS", "zip": "\\d{5}", "zipex": "11600"},
  "data/UZ": {"fmt": "%N%n%O%n%A%n%Z %C%n%S", "id": "data/UZ", "key": "UZ", "name": "UZBEKISTAN", "posturl": "http://www.pochta.uz/ru/uslugi/indexsearch.html", "upper": "CS", "zip": "\\d{6}", "zipex": "702100,700000"},
  "data/VA": {"fmt": "%N%n%O%n%A%n%Z %C", "id": "data/VA", "key": "VA", "name": "VATICAN", "zip": "00120", "zipex": "00120"},
  "data/VC": {"fmt": "%N%n%O%n%A%n%C %Z", "id": "data/VC", "key": "VC", "name": "SAINT VINCENT AND THE GRENADINES (ANTILLES)", "posturl": "http://www.svgpost.gov.vc/?option=com_content&view=article&id=3&Itemid=16", "zip": "VC\\d{4}", "zipex": "VC0100,VC0110,VC0400"},
  "data/VE": {"fmt": "%N%n%O%n%A%n%C %Z, %S", "id": "data/VE", "key": "VE", "lang": "es", "languages": "es", "name": "VENEZUELA", "posturl": "http://www.ipostel.gob.ve/index.php/oficinas-postales", "require": "ACS", "state_name_type": "state", "sub_isoids": "Z~B~C~D~E~F~G~H~Y~W~A~I~J~K~L~M~N~O~P~R~S~T~X~U~V", "sub_keys": "Amazonas~Anzoátegui~Apure~Aragua~Barinas~Bolívar~Carabobo~Cojedes~Delta Amacuro~Dependencias Federales~Distrito Federal~Falcón~Guárico~Lara~Mérida~Miranda~Monagas~Nueva Esparta~Portuguesa~Sucre~Táchira~Trujillo~Vargas~Yaracuy~Zulia", "upper": "CS", "zip": "\\d{4}", "zipex": "1010,3001,8011,1020"},
  "data/VG": {"fmt": "%N%n%O%n%A%n%C%n%Z", "id": "data/VG", "key": "VG", "name": "VIRGIN ISLANDS (BRITISH)", "require": "A", "zip": "VG\\d{4}", "zipex": "VG1110,VG1150,VG1160"},
  "data/VI": {"fmt": "%N%n%O%n%A%n%C %S %Z", "id": "data/VI", "key": "VI", "name": "VIRGIN ISLANDS (U.S.)", "posturl": "http://zip4.usps.com/zip4/welcome.jsp", "require": "ACSZ", "state_name_type": "state", "upper": "ACNOS", "zip": "(008(?:(?:[0-4]\\d)|(?:5[01])))(?:[ \\-](\\d{4}))?", "zip_name_type": "zip", "zipex": "00802-1222,00850-9802"},
  "data/VN": {"fmt": "%N%n%O%n%A%n%C%n%S %Z", "id": "data/VN", "key": "VN", "lang": "vi", "languages": "vi", "lfmt": "%N%n%O%n%A%n%C%n%S %Z", "name": "VIET NAM", "posturl": "http://postcode.vnpost.vn/services/search.aspx", "sub_isoids": "44~43~55~54~53~56~50~57~31~58~40~59~04~CT~DN~33~72~71~39~45~30~03~63~HN~23~61~HP~73~14~66~34~47~28~01~09~02~35~41~67~22~18~36~68~32~24~27~29~13~25~52~05~37~20~69~21~SG~26~46~51~07~49~70~06", "sub_keys": "An Giang~Bà Rịa–Vũng Tàu~Bạc Liêu~Bắc Giang~Bắc Kạn~Bắc Ninh~Bến Tre~Bình Dương~Bình Định~Bình Phước~Bình Thuận~Cà Mau~Cao Bằng~Cần Thơ~Đà Nẵng~Đắk Lắk~Đăk Nông~Điện Biên~Đồng Nai~Đồng Tháp~Gia Lai~Hà Giang~Hà Nam~Hà Nội~Hà Tĩnh~Hải Dương~Hải Phòng~Hậu Giang~Hòa Bình~Hưng Yên~Khánh Hòa~Kiên Giang~Kon Tum~Lai Châu~Lạng Sơn~Lào Cai~Lâm Đồng~Long An~Nam Định~Nghệ An~Ninh Bình~Ninh Thuận~Phú Thọ~Phú Yên~Quảng Bình~Quảng Nam~Quảng Ngãi~Quảng Ninh~Quảng Trị~Sóc Trăng~Sơn La~Tây Ninh~Thái Bình~Thái Nguyên~Thanh Hóa~Thành phố Hồ Chí Minh~Thừa Thiên–Huế~Tiền Giang~Trà Vinh~Tuyên Quang~Vĩnh Long~Vĩnh Phúc~Yên Bái", "sub_lnames": "An Giang Province~Ba Ria-Vung Tau Province~Bac Lieu Province~Bac Giang Province~Bac Kan Province~Bac Ninh Province~Ben Tre Province~Binh Duong Province~Binh Dinh Province~Binh Phuoc Province~Binh Thuan Province~Ca Mau Province~Cao Bang Province~Can Tho City~Da Nang City~Dak Lak Province~Dak Nong Province~Dien Bien Province~Dong Nai Province~Dong Thap Province~Gia Lai Province~Ha Giang Province~Ha Nam Province~Hanoi City~Ha Tinh Province~Hai Duong Province~Haiphong City~Hau Giang Province~Hoa Binh Province~Hung Yen Province~Khanh Hoa Province~Kien Giang Province~Kon Tum Province~Lai Chau Province~Lang Song Province~Lao Cai Province~Lam Dong Province~Long An Province~Nam Dinh Province~Nghe An Province~Ninh Binh Province~Ninh Thuan Province~Phu Tho Province~Phu Yen Province~Quang Binh Province~Quang Nam Province~Quang Ngai Province~Quang Ninh Province~Quang Tri Province~Soc Trang Province~Son La Province~Tay Ninh Province~Thai Binh Province~Thai Nguyen Province~Thanh Hoa Province~Ho Chi Minh City~Thua Thien-Hue Province~Tien Giang Province~Tra Vinh Province~Tuyen Quang Province~Vinh Long Province~Vinh Phuc Province~Yen Bai Province", "zip": "\\d{5}\\d?", "zipex": "70010,55999"},
  "data/VU": {"id": "data/VU", "key": "VU", "name": "VANUATU"},
  "data/WF": {"fmt": "%O%n%N%n%A%n%Z %C %X", "id": "data/WF", "key": "WF", "name": "WALLIS AND FUTUNA ISLANDS", "require": "ACZ", "upper": "ACX", "zip": "986\\d{2}", "zipex": "98600"},
  "data/WS": {"id": "data/WS", "key": "WS", "name": "SAMOA"},
  "data/XK": {"fmt": "%N%n%O%n%A%n%Z %C", "id": "data/XK", "key": "XK", "name": "KOSOVO", "zip": "[1-7]\\d{4}", "zipex": "10000"},
  "data/YE": {"id": "data/YE", "key": "YE", "name": "YEMEN"},
  "data/YT": {"fmt": "%O%n%N%n%A%n%Z %C %X", "id": "data/YT", "key": "YT", "name": "MAYOTTE", "require": "ACZ", "upper": "ACX", "zip": "976\\d{2}", "zipex": "97600"},
  "data/ZA": {"fmt": "%N%n%O%n%A%n%D%n%C%n%Z", "id": "data/ZA", "key": "ZA", "name": "SOUTH AFRICA", "posturl": "https://www.postoffice.co.za/Questions/postalcode.html", "require": "ACZ", "zip": "\\d{4}", "zipex": "0083,1451,0001"},
  "data/ZM": {"fmt": "%N%n%O%n%A%n%Z %C", "id": "data/ZM", "key": "ZM", "name": "ZAMBIA", "zip": "\\d{5}", "zipex": "50100,50101"},
  "data/ZW": {"id": "data/ZW", "key": "ZW", "name": "ZIMBABWE"},
};
PK
!<ÑcZ2chrome/res/addressmetadata/addressReferencesExt.js/* 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 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"],
    fmt: "%N%n%A%n%C%S%n%Z%O",
  },
};
PK
!<ܬ«º™™'chrome/res/autocomplete-item-shared.css/* 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/. */

@namespace url("http://www.w3.org/1999/xhtml");
@namespace xul url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");


xul|richlistitem[originaltype="autofill-profile"][selected="true"] > .autofill-item-box {
  background-color: #F2F2F2;
}

xul|richlistitem[originaltype="autofill-footer"][selected="true"] > .autofill-item-box > .autofill-button,
xul|richlistitem[originaltype="autofill-clear-button"][selected="true"] > .autofill-item-box > .autofill-button {
  background-color: #DCDCDE;
}

xul|richlistitem[originaltype="autofill-insecureWarning"] {
  border-bottom: 1px solid var(--panel-separator-color);
  background-color: var(--arrowpanel-dimmed);
}

.autofill-item-box {
  --item-padding-vertical: 7px;
  --item-padding-horizontal: 10px;
  --col-spacer: 7px;
  --item-width: calc(50% - (var(--col-spacer) / 2));
  --label-text-color: #262626;
  --comment-text-color: #646464;
  --warning-text-color: #646464;
  --btn-text-color: -moz-FieldText;

  --default-font-size: 12;
  --label-affix-font-size: 10;
  --label-font-size: 12;
  --comment-font-size: 10;
  --warning-font-size: 10;
  --btn-font-size: 11;
}

.autofill-item-box[size="small"] {
  --item-padding-vertical: 7px;
  --col-spacer: 0px;
  --row-spacer: 3px;
  --item-width: 100%;
}

.autofill-item-box:not([ac-image=""]) {
  --item-padding-vertical: 6.5px;
  --comment-font-size: 11;
}

.autofill-footer,
.autofill-footer[size="small"] {
  --item-width: 100%;
  --item-padding-vertical: 0;
  --item-padding-horizontal: 0;
}

.autofill-item-box {
  box-sizing: border-box;
  margin: 0;
  border-bottom: 1px solid rgba(38,38,38,.15);
  padding: var(--item-padding-vertical) 0;
  padding-inline-start: var(--item-padding-horizontal);
  padding-inline-end: var(--item-padding-horizontal);
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
  background-color: #FFFFFF;
  color: var(--label-text-color);
}

.autofill-item-box:last-child {
  border-bottom: 0;
}

.autofill-item-box > .profile-item-col {
  box-sizing: border-box;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  width: var(--item-width);
}

.autofill-item-box > .profile-label-col {
  text-align: start;
}

.autofill-item-box:not([ac-image=""]) > .profile-label-col::before {
  margin-right: 5px;
  float: left;
  content: "";
  width: 16px;
  height: 16px;
  background-image: var(--primary-icon);
  background-size: 16px 16px;
  -moz-context-properties: fill;
  fill: #4D4D4D;
}

.autofill-item-box > .profile-label-col > .profile-label {
  font-size: calc(var(--label-font-size) / var(--default-font-size) * 1em);
}

.autofill-item-box > .profile-label-col > .profile-label-affix {
  font-weight: lighter;
  font-size: calc(var(--label-affix-font-size) / var(--default-font-size) * 1em);
}

.autofill-item-box > .profile-comment-col {
  margin-inline-start: var(--col-spacer);
  text-align: end;
  color: var(--comment-text-color);
}

.autofill-item-box > .profile-comment-col > .profile-comment {
  font-size: calc(var(--comment-font-size) / var(--default-font-size) * 1em);
}

.autofill-item-box[size="small"] {
  flex-direction: column;
}

.autofill-item-box[size="small"] > .profile-comment-col {
  margin-top: var(--row-spacer);
  text-align: start;
}

.autofill-footer {
  padding: 0;
  flex-direction: column;
}

.autofill-footer > .autofill-footer-row {
  display: flex;
  justify-content: center;
  align-items: center;
  width: var(--item-width);
}

.autofill-footer > .autofill-warning {
  padding: 2.5px 0;
  color: var(--warning-text-color);
  text-align: center;
  background-color: rgba(248,232,28,.2);
  border-bottom: 1px solid rgba(38,38,38,.15);
  font-size: calc(var(--warning-font-size) / var(--default-font-size) * 1em);
}

.autofill-footer > .autofill-button {
  box-sizing: border-box;
  padding: 0 10px;
  min-height: 40px;
  background-color: #EDEDED;
  font-size: calc(var(--btn-font-size) / var(--default-font-size) * 1em);
  color: var(--btn-text-color);
  text-align: center;
}

.autofill-footer[no-warning="true"] > .autofill-warning {
  display: none;
}

.autofill-insecure-item {
  box-sizing: border-box;
  padding: 4px 0;
  display: flex;
  flex-direction: row;
  flex-wrap: nowrap;
  align-items: center;
  color: GrayText;
}

.autofill-insecure-item::before {
  display: block;
  margin-inline-start: 4px;
  margin-inline-end: 8px;
  content: "";
  width: 16px;
  height: 16px;
  background-image: url(chrome://browser/skin/connection-mixed-active-loaded.svg);
  -moz-context-properties: fill;
  fill: GrayText;
}
PK
!<ûg“55 chrome/res/autocomplete-item.css/* 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/. */

@namespace url("http://www.w3.org/1999/xhtml");


.autofill-item-box {
  --default-font-size: 14.25;
}
PK
!<PÉڌchrome/res/editAddress.css/* 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/. */

.editAddressForm {
  display: flex;
  flex-wrap: wrap;
  /* Use space-between so --grid-column-row-gap is in between the elements on a row */
  justify-content: space-between;
}

:root:not([subdialog]) .editAddressForm {
  margin-inline-start: calc(var(--grid-column-row-gap) / -2);
  margin-inline-end: calc(var(--grid-column-row-gap) / -2);
}

.editAddressForm .container {
  /* !important is needed to override preferences.css's generic label rule. */
  margin-top: var(--grid-column-row-gap) !important;
  margin-inline-start: calc(var(--grid-column-row-gap) / 2);
  margin-inline-end: calc(var(--grid-column-row-gap) / 2);
  flex-grow: 1;
}

#country-container {
  /* The country dropdown has a different intrinsic (content) width than the
     other fields which are <input>. */
  flex-basis: calc(50% - var(--grid-column-row-gap));
  flex-grow: 0;
  /* Country names can be longer than 50% which ruins the symmetry in the grid. */
  max-width: calc(50% - var(--grid-column-row-gap));
}


/* Begin name field rules */

#name-container input {
  /* Override the default @size="20" on <input>, which acts like a min-width, not
   * allowing the fields to shrink with flexbox as small as they need to to match
   * the other rows. This is noticeable on narrow viewports e.g. in the
   * PaymentRequest dialog on Linux due to the larger font-size. */
  width: 0;
}

/* When there is focus within any of the name fields, the border of the inputs
 * should be the focused color, except for inner ones which get overriden below. */
#name-container:focus-within input {
  border-color: var(--in-content-border-focus);
}

/* Invalid name fields should show the error outline instead of the focus border */
#name-container:focus-within input:-moz-ui-invalid {
  border-color: transparent;
}

#given-name-container,
#additional-name-container,
#family-name-container {
  display: flex;
  /* The 3 pieces inside the name container don't have the .container class so
     need to set flex-grow themselves. See `.editAddressForm .container` */
  flex-grow: 1;
  /* Remove the bottom margin from the name containers so that the outer
     #name-container provides the margin on the outside */
  margin-bottom: 0 !important;
  margin-left: 0;
  margin-right: 0;
}

/* The name fields are placed adjacent to each other.
   Remove the border-radius on adjacent fields. */
#given-name:dir(ltr),
#family-name:dir(rtl) {
  border-top-right-radius: 0;
  border-bottom-right-radius: 0;
  border-right-width: 0;
}

#given-name:dir(rtl),
#family-name:dir(ltr) {
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
  border-left-width: 0;
}

#additional-name {
  border-radius: 0;
  /* This provides the inner separators between the fields and should never
   * change to the focused color. */
  border-left-color: var(--in-content-box-border-color) !important;
  border-right-color: var(--in-content-box-border-color) !important;
}

/* Since the name fields are adjacent, there isn't room for the -moz-ui-invalid
   box-shadow so raise invalid name fields and their labels above the siblings
   so the shadow is shown around all 4 sides. */
#name-container input:-moz-ui-invalid,
#name-container input:-moz-ui-invalid ~ .label-text {
  z-index: 1;
}

/* End name field rules */

#name-container,
#street-address-container {
  /* Name and street address are always full-width */
  flex: 0 1 100%;
}

#street-address {
  resize: vertical;
}

#country-warning-message {
  box-sizing: border-box;
  font-size: 1rem;
  align-items: center;
  text-align: start;
  color: #737373;
  padding-inline-start: 1em;
}

:root:not([subdialog]) #country-warning-message {
  display: none;
}
PK
!<qû²ÅÅchrome/res/editCreditCard.css/* 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/. */

.editCreditCardForm {
  display: grid;
  grid-template-areas:
    "cc-number          cc-exp-month       cc-exp-year"
    "cc-name            cc-type            cc-csc"
    "billingAddressGUID billingAddressGUID billingAddressGUID";
  grid-template-columns: 4fr 2fr 2fr;
  grid-row-gap: var(--grid-column-row-gap);
  grid-column-gap: var(--grid-column-row-gap);
}

.editCreditCardForm label {
  /* Remove the margin on these labels since they are styled on top of
     the input/select element. */
  margin-inline-start: 0;
  margin-inline-end: 0;
}

.editCreditCardForm .container {
  display: flex;
}

#cc-number-container {
  grid-area: cc-number;
}

#cc-exp-month-container {
  grid-area: cc-exp-month;
}

#cc-exp-year-container {
  grid-area: cc-exp-year;
}

#cc-name-container {
  grid-area: cc-name;
}

#cc-type-container {
  grid-area: cc-type;
}

#cc-csc-container {
  grid-area: cc-csc;
}

#billingAddressGUID-container {
  grid-area: billingAddressGUID;
}

#billingAddressGUID {
  grid-area: dropdown;
}
PK
!<bá²3		 chrome/res/editDialog-shared.css/* 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/. */

:root {
  --in-field-label-size: .8em;
  --grid-column-row-gap: 8px;
  /* Use the animation-easing-function that is defined in xul.css. */
  --animation-easing-function: cubic-bezier(.07,.95,0,1);
}

:root[subdialog] form {
  /* Add extra space to ensure invalid input box is displayed properly */
  padding: 2px;
}

/* The overly specific input attributes are required to override
   padding from common.css */
form input[type="email"],
form input[type="tel"],
form input[type="text"],
form textarea,
form select {
  flex-grow: 1;
  padding-top: calc(var(--in-field-label-size) + .4em);
}

select {
  margin: 0;
  padding-bottom: 5px;
}

form label,
form div {
  /* Positioned so that the .label-text and .error-text children will be
     positioned relative to this. */
  position: relative;
  display: block;
  line-height: 1em;
}

form :-moz-any(label, div) .label-text {
  position: absolute;
  color: GrayText;
  pointer-events: none;
  left: 10px;
  top: .2em;
  transition: top .2s var(--animation-easing-function),
              font-size .2s var(--animation-easing-function);
}

form :-moz-any(label, div):focus-within .label-text,
form :-moz-any(label, div) .label-text[field-populated] {
  top: 0;
  font-size: var(--in-field-label-size);
}

form :-moz-any(input, select, textarea):focus ~ .label-text {
  color: var(--in-content-item-selected);
}

/* Focused error fields should get a darker text but not the blue one since it
 * doesn't look good with the red error outline. */
form :-moz-any(input, select, textarea):focus:-moz-ui-invalid ~ .label-text {
  color: var(--in-content-text-color);
}

form div[required] > label .label-text::after,
form :-moz-any(label, div)[required] .label-text::after {
  content: attr(fieldRequiredSymbol);
}

.persist-checkbox label {
  display: flex;
  flex-direction: row;
  align-items: center;
  margin-top: var(--grid-column-row-gap);
  margin-bottom: var(--grid-column-row-gap);
}

:root[subdialog] form {
  /* Match the margin-inline-start of the #controls-container buttons
     and provide enough padding at the top of the form so button outlines
     don't get clipped. */
  padding: 4px 4px 0;
}

#controls-container {
  flex: 0 1 100%;
  justify-content: end;
  margin: 1em 0 0;
}
PK
!<7Dâchrome/res/editDialog.css/* 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/. */

/* Linux specific rules */
:root[subdialog] body {
  font-size: 0.85rem;
}
PK
!<¼âü??+chrome/res/phonenumberutils/PhoneNumber.jsm/* This Source Code Form is subject to the terms of the Apache License, Version
 * 2.0. If a copy of the Apache License was not distributed with this file, You
 * can obtain one at https://www.apache.org/licenses/LICENSE-2.0 */

// This library came from https://github.com/andreasgal/PhoneNumber.js but will
// be further maintained by our own in Form Autofill codebase.

"use strict";

var EXPORTED_SYMBOLS = ["PhoneNumber"];

ChromeUtils.defineModuleGetter(this, "PHONE_NUMBER_META_DATA",
                               "resource://formautofill/phonenumberutils/PhoneNumberMetaData.jsm");
ChromeUtils.defineModuleGetter(this, "PhoneNumberNormalizer",
                               "resource://formautofill/phonenumberutils/PhoneNumberNormalizer.jsm");
var PhoneNumber = (function(dataBase) {
  const MAX_PHONE_NUMBER_LENGTH = 50;
  const NON_ALPHA_CHARS = /[^a-zA-Z]/g;
  const NON_DIALABLE_CHARS = /[^,#+\*\d]/g;
  const NON_DIALABLE_CHARS_ONCE = new RegExp(NON_DIALABLE_CHARS.source);
  const SPLIT_FIRST_GROUP = /^(\d+)(.*)$/;
  const LEADING_PLUS_CHARS_PATTERN = /^[+\uFF0B]+/g;

  // Format of the string encoded meta data. If the name contains "^" or "$"
  // we will generate a regular expression from the value, with those special
  // characters as prefix/suffix.
  const META_DATA_ENCODING = [
    "region",
    "^(?:internationalPrefix)",
    "nationalPrefix",
    "^(?:nationalPrefixForParsing)",
    "nationalPrefixTransformRule",
    "nationalPrefixFormattingRule",
    "^possiblePattern$",
    "^nationalPattern$",
    "formats",
  ];

  const FORMAT_ENCODING = [
    "^pattern$",
    "nationalFormat",
    "^leadingDigits",
    "nationalPrefixFormattingRule",
    "internationalFormat",
  ];

  let regionCache = Object.create(null);

  // Parse an array of strings into a convenient object. We store meta
  // data as arrays since thats much more compact than JSON.
  function ParseArray(array, encoding, obj) {
    for (let n = 0; n < encoding.length; ++n) {
      let value = array[n];
      if (!value) {
        continue;
      }
      let field = encoding[n];
      let fieldAlpha = field.replace(NON_ALPHA_CHARS, "");
      if (field != fieldAlpha) {
        value = new RegExp(field.replace(fieldAlpha, value));
      }
      obj[fieldAlpha] = value;
    }
    return obj;
  }

  // Parse string encoded meta data into a convenient object
  // representation.
  function ParseMetaData(countryCode, md) {
    let array = JSON.parse(md);
    md = ParseArray(array,
                    META_DATA_ENCODING,
                    {countryCode});
    regionCache[md.region] = md;
    return md;
  }

  // Parse string encoded format data into a convenient object
  // representation.
  function ParseFormat(md) {
    let formats = md.formats;
    if (!formats) {
      return;
    }
    // Bail if we already parsed the format definitions.
    if (!(Array.isArray(formats[0]))) {
      return;
    }
    for (let n = 0; n < formats.length; ++n) {
      formats[n] = ParseArray(formats[n],
                              FORMAT_ENCODING,
                              {});
    }
  }

  // Search for the meta data associated with a region identifier ("US") in
  // our database, which is indexed by country code ("1"). Since we have
  // to walk the entire database for this, we cache the result of the lookup
  // for future reference.
  function FindMetaDataForRegion(region) {
    // Check in the region cache first. This will find all entries we have
    // already resolved (parsed from a string encoding).
    let md = regionCache[region];
    if (md) {
      return md;
    }
    for (let countryCode in dataBase) {
      let entry = dataBase[countryCode];
      // Each entry is a string encoded object of the form '["US..', or
      // an array of strings. We don't want to parse the string here
      // to save memory, so we just substring the region identifier
      // and compare it. For arrays, we compare against all region
      // identifiers with that country code. We skip entries that are
      // of type object, because they were already resolved (parsed into
      // an object), and their country code should have been in the cache.
      if (Array.isArray(entry)) {
        for (let n = 0; n < entry.length; n++) {
          if (typeof entry[n] == "string" && entry[n].substr(2, 2) == region) {
            if (n > 0) {
              // Only the first entry has the formats field set.
              // Parse the main country if we haven't already and use
              // the formats field from the main country.
              if (typeof entry[0] == "string") {
                entry[0] = ParseMetaData(countryCode, entry[0]);
              }
              let formats = entry[0].formats;
              let current = ParseMetaData(countryCode, entry[n]);
              current.formats = formats;
              entry[n] = current;
              return entry[n];
            }

            entry[n] = ParseMetaData(countryCode, entry[n]);
            return entry[n];
          }
        }
        continue;
      }
      if (typeof entry == "string" && entry.substr(2, 2) == region) {
        dataBase[countryCode] = ParseMetaData(countryCode, entry);
        return dataBase[countryCode];
      }
    }
  }

  // Format a national number for a given region. The boolean flag "intl"
  // indicates whether we want the national or international format.
  function FormatNumber(regionMetaData, number, intl) {
    // We lazily parse the format description in the meta data for the region,
    // so make sure to parse it now if we haven't already done so.
    ParseFormat(regionMetaData);
    let formats = regionMetaData.formats;
    if (!formats) {
      return null;
    }
    for (let n = 0; n < formats.length; ++n) {
      let format = formats[n];
      // The leading digits field is optional. If we don't have it, just
      // use the matching pattern to qualify numbers.
      if (format.leadingDigits && !format.leadingDigits.test(number)) {
        continue;
      }
      if (!format.pattern.test(number)) {
        continue;
      }
      if (intl) {
        // If there is no international format, just fall back to the national
        // format.
        let internationalFormat = format.internationalFormat;
        if (!internationalFormat) {
          internationalFormat = format.nationalFormat;
        }
        // Some regions have numbers that can't be dialed from outside the
        // country, indicated by "NA" for the international format of that
        // number format pattern.
        if (internationalFormat == "NA") {
          return null;
        }
        // Prepend "+" and the country code.
        number = "+" + regionMetaData.countryCode + " " +
                 number.replace(format.pattern, internationalFormat);
      } else {
        number = number.replace(format.pattern, format.nationalFormat);
        // The region has a national prefix formatting rule, and it can be overwritten
        // by each actual number format rule.
        let nationalPrefixFormattingRule = regionMetaData.nationalPrefixFormattingRule;
        if (format.nationalPrefixFormattingRule) {
          nationalPrefixFormattingRule = format.nationalPrefixFormattingRule;
        }
        if (nationalPrefixFormattingRule) {
          // The prefix formatting rule contains two magic markers, "$NP" and "$FG".
          // "$NP" will be replaced by the national prefix, and "$FG" with the
          // first group of numbers.
          let match = number.match(SPLIT_FIRST_GROUP);
          if (match) {
            let firstGroup = match[1];
            let rest = match[2];
            let prefix = nationalPrefixFormattingRule;
            prefix = prefix.replace("$NP", regionMetaData.nationalPrefix);
            prefix = prefix.replace("$FG", firstGroup);
            number = prefix + rest;
          }
        }
      }
      return (number == "NA") ? null : number;
    }
    return null;
  }

  function NationalNumber(regionMetaData, number) {
    this.region = regionMetaData.region;
    this.regionMetaData = regionMetaData;
    this.number = number;
  }

  // NationalNumber represents the result of parsing a phone number. We have
  // three getters on the prototype that format the number in national and
  // international format. Once called, the getters put a direct property
  // onto the object, caching the result.
  NationalNumber.prototype = {
    // +1 949-726-2896
    get internationalFormat() {
      let value = FormatNumber(this.regionMetaData, this.number, true);
      Object.defineProperty(this, "internationalFormat", {value, enumerable: true});
      return value;
    },
    // (949) 726-2896
    get nationalFormat() {
      let value = FormatNumber(this.regionMetaData, this.number, false);
      Object.defineProperty(this, "nationalFormat", {value, enumerable: true});
      return value;
    },
    // +19497262896
    get internationalNumber() {
      let value = this.internationalFormat ? this.internationalFormat.replace(NON_DIALABLE_CHARS, "")
                                           : null;
      Object.defineProperty(this, "internationalNumber", {value, enumerable: true});
      return value;
    },
    // 9497262896
    get nationalNumber() {
      let value = this.nationalFormat ? this.nationalFormat.replace(NON_DIALABLE_CHARS, "")
                                      : null;
      Object.defineProperty(this, "nationalNumber", {value, enumerable: true});
      return value;
    },
    // country name 'US'
    get countryName() {
      let value = this.region ? this.region : null;
      Object.defineProperty(this, "countryName", {value, enumerable: true});
      return value;
    },
    // country code '+1'
    get countryCode() {
      let value = this.regionMetaData.countryCode ? "+" + this.regionMetaData.countryCode : null;
      Object.defineProperty(this, "countryCode", {value, enumerable: true});
      return value;
    },
  };

  // Check whether the number is valid for the given region.
  function IsValidNumber(number, md) {
    return md.possiblePattern.test(number);
  }

  // Check whether the number is a valid national number for the given region.
  /* eslint-disable no-unused-vars */
  function IsNationalNumber(number, md) {
    return IsValidNumber(number, md) && md.nationalPattern.test(number);
  }

  // Determine the country code a number starts with, or return null if
  // its not a valid country code.
  function ParseCountryCode(number) {
    for (let n = 1; n <= 3; ++n) {
      let cc = number.substr(0, n);
      if (dataBase[cc]) {
        return cc;
      }
    }
    return null;
  }

  // Parse a national number for a specific region. Return null if the
  // number is not a valid national number (it might still be a possible
  // number for parts of that region).
  function ParseNationalNumber(number, md) {
    if (!md.possiblePattern.test(number) ||
        !md.nationalPattern.test(number)) {
      return null;
    }
    // Success.
    return new NationalNumber(md, number);
  }

  function ParseNationalNumberAndCheckNationalPrefix(number, md) {
    let ret;

    // This is not an international number. See if its a national one for
    // the current region. National numbers can start with the national
    // prefix, or without.
    if (md.nationalPrefixForParsing) {
      // Some regions have specific national prefix parse rules. Apply those.
      let withoutPrefix = number.replace(md.nationalPrefixForParsing,
                                         md.nationalPrefixTransformRule || "");
      ret = ParseNationalNumber(withoutPrefix, md);
      if (ret) {
        return ret;
      }
    } else {
      // If there is no specific national prefix rule, just strip off the
      // national prefix from the beginning of the number (if there is one).
      let nationalPrefix = md.nationalPrefix;
      if (nationalPrefix && number.indexOf(nationalPrefix) == 0 &&
          (ret = ParseNationalNumber(number.substr(nationalPrefix.length), md))) {
        return ret;
      }
    }
    ret = ParseNationalNumber(number, md);
    if (ret) {
      return ret;
    }
  }

  function ParseNumberByCountryCode(number, countryCode) {
    let ret;

    // Lookup the meta data for the region (or regions) and if the rest of
    // the number parses for that region, return the parsed number.
    let entry = dataBase[countryCode];
    if (Array.isArray(entry)) {
      for (let n = 0; n < entry.length; ++n) {
        if (typeof entry[n] == "string") {
          entry[n] = ParseMetaData(countryCode, entry[n]);
        }
        if (n > 0) {
          entry[n].formats = entry[0].formats;
        }
        ret = ParseNationalNumberAndCheckNationalPrefix(number, entry[n]);
        if (ret) {
          return ret;
        }
      }
      return null;
    }
    if (typeof entry == "string") {
      entry = dataBase[countryCode] = ParseMetaData(countryCode, entry);
    }
    return ParseNationalNumberAndCheckNationalPrefix(number, entry);
  }

  // Parse an international number that starts with the country code. Return
  // null if the number is not a valid international number.
  function ParseInternationalNumber(number) {
    // Parse and strip the country code.
    let countryCode = ParseCountryCode(number);
    if (!countryCode) {
      return null;
    }
    number = number.substr(countryCode.length);

    return ParseNumberByCountryCode(number, countryCode);
  }

  // Parse a number and transform it into the national format, removing any
  // international dial prefixes and country codes.
  function ParseNumber(number, defaultRegion) {
    let ret;

    // Remove formating characters and whitespace.
    number = PhoneNumberNormalizer.Normalize(number);

    // If there is no defaultRegion or the defaultRegion is the global region,
    // we can't parse international access codes.
    if ((!defaultRegion || defaultRegion === "001") && number[0] !== "+") {
      return null;
    }

    // Detect and strip leading '+'.
    if (number[0] === "+") {
      return ParseInternationalNumber(number.replace(LEADING_PLUS_CHARS_PATTERN, ""));
    }

    // If "defaultRegion" is a country code, use it to parse the number directly.
    let matches = String(defaultRegion).match(/^\+?(\d+)/);
    if (matches) {
      let countryCode = ParseCountryCode(matches[1]);
      if (!countryCode) {
        return null;
      }
      return ParseNumberByCountryCode(number, countryCode);
    }

    // Lookup the meta data for the given region.
    let md = FindMetaDataForRegion(defaultRegion.toUpperCase());

    if (!md) {
      dump("Couldn't find Meta Data for region: " + defaultRegion + "\n");
      return null;
    }

    // See if the number starts with an international prefix, and if the
    // number resulting from stripping the code is valid, then remove the
    // prefix and flag the number as international.
    if (md.internationalPrefix.test(number)) {
      let possibleNumber = number.replace(md.internationalPrefix, "");
      ret = ParseInternationalNumber(possibleNumber);
      if (ret) {
        return ret;
      }
    }

    ret = ParseNationalNumberAndCheckNationalPrefix(number, md);
    if (ret) {
      return ret;
    }

    // Now lets see if maybe its an international number after all, but
    // without '+' or the international prefix.
    ret = ParseInternationalNumber(number);
    if (ret) {
      return ret;
    }

    // If the number matches the possible numbers of the current region,
    // return it as a possible number.
    if (md.possiblePattern.test(number)) {
      return new NationalNumber(md, number);
    }

    // We couldn't parse the number at all.
    return null;
  }

  function IsPlainPhoneNumber(number) {
    if (typeof number !== "string") {
      return false;
    }

    let length = number.length;
    let isTooLong = (length > MAX_PHONE_NUMBER_LENGTH);
    let isEmpty = (length === 0);
    return !(isTooLong || isEmpty || NON_DIALABLE_CHARS_ONCE.test(number));
  }

  return {
    IsPlain: IsPlainPhoneNumber,
    Parse: ParseNumber,
  };
})(PHONE_NUMBER_META_DATA);
PK
!<ôg`RÚÚ3chrome/res/phonenumberutils/PhoneNumberMetaData.jsm/* This Source Code Form is subject to the terms of the Apache License, Version
 * 2.0. If a copy of the Apache License was not distributed with this file, You
 * can obtain one at https://www.apache.org/licenses/LICENSE-2.0 */

/*
 * This data was generated base on libphonenumber v8.4.1 via the script in
 * https://github.com/andreasgal/PhoneNumber.js
 *
 * The XML format of libphonenumber has changed since v8.4.2 so we can only stay
 * in this version for now.
 */

var EXPORTED_SYMBOLS = ["PHONE_NUMBER_META_DATA"];

var PHONE_NUMBER_META_DATA = {
"46": '["SE","00","0",null,null,"$NP$FG","\\\\d{6,12}","[1-35-9]\\\\d{5,11}|4\\\\d{6,8}",[["(8)(\\\\d{2,3})(\\\\d{2,3})(\\\\d{2})","$1-$2 $3 $4","8",null,"$1 $2 $3 $4"],["([1-69]\\\\d)(\\\\d{2,3})(\\\\d{2})(\\\\d{2})","$1-$2 $3 $4","1[013689]|2[0136]|3[1356]|4[0246]|54|6[03]|90",null,"$1 $2 $3 $4"],["([1-469]\\\\d)(\\\\d{3})(\\\\d{2})","$1-$2 $3","1[136]|2[136]|3[356]|4[0246]|6[03]|90",null,"$1 $2 $3"],["(\\\\d{3})(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1-$2 $3 $4","1[2457]|2(?:[247-9]|5[0138])|3[0247-9]|4[1357-9]|5[0-35-9]|6(?:[124-689]|7[0-2])|9(?:[125-8]|3[0-5]|4[0-3])",null,"$1 $2 $3 $4"],["(\\\\d{3})(\\\\d{2,3})(\\\\d{2})","$1-$2 $3","1[2457]|2(?:[247-9]|5[0138])|3[0247-9]|4[1357-9]|5[0-35-9]|6(?:[124-689]|7[0-2])|9(?:[125-8]|3[0-5]|4[0-3])",null,"$1 $2 $3"],["(7\\\\d)(\\\\d{3})(\\\\d{2})(\\\\d{2})","$1-$2 $3 $4","7",null,"$1 $2 $3 $4"],["(77)(\\\\d{2})(\\\\d{2})","$1-$2$3","7",null,"$1 $2 $3"],["(20)(\\\\d{2,3})(\\\\d{2})","$1-$2 $3","20",null,"$1 $2 $3"],["(9[034]\\\\d)(\\\\d{2})(\\\\d{2})(\\\\d{3})","$1-$2 $3 $4","9[034]",null,"$1 $2 $3 $4"],["(9[034]\\\\d)(\\\\d{4})","$1-$2","9[034]",null,"$1 $2"],["(\\\\d{3})(\\\\d{2})(\\\\d{3})(\\\\d{2})(\\\\d{2})","$1-$2 $3 $4 $5","25[245]|67[3-6]",null,"$1 $2 $3 $4 $5"]]]',
"299": '["GL","00",null,null,null,null,"\\\\d{6}","[1-689]\\\\d{5}",[["(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2 $3",null,null]]]',
"385": '["HR","00","0",null,null,"$NP$FG","\\\\d{6,9}","[1-7]\\\\d{5,8}|[89]\\\\d{6,8}",[["(1)(\\\\d{4})(\\\\d{3})","$1 $2 $3","1",null],["([2-5]\\\\d)(\\\\d{3})(\\\\d{3,4})","$1 $2 $3","[2-5]",null],["(9\\\\d)(\\\\d{3})(\\\\d{3,4})","$1 $2 $3","9",null],["(6[01])(\\\\d{2})(\\\\d{2,3})","$1 $2 $3","6[01]",null],["([67]\\\\d)(\\\\d{3})(\\\\d{3,4})","$1 $2 $3","[67]",null],["(80[01])(\\\\d{2})(\\\\d{2,3})","$1 $2 $3","8",null],["(80[01])(\\\\d{3})(\\\\d{3})","$1 $2 $3","8",null]]]',
"670": '["TL","00",null,null,null,null,"\\\\d{7,8}","[2-489]\\\\d{6}|7\\\\d{6,7}",[["(\\\\d{3})(\\\\d{4})","$1 $2","[2-489]",null],["(\\\\d{4})(\\\\d{4})","$1 $2","7",null]]]',
"258": '["MZ","00",null,null,null,null,"\\\\d{8,9}","[28]\\\\d{7,8}",[["([28]\\\\d)(\\\\d{3})(\\\\d{3,4})","$1 $2 $3","2|8[2-7]",null],["(80\\\\d)(\\\\d{3})(\\\\d{3})","$1 $2 $3","80",null]]]',
"359": '["BG","00","0",null,null,"$NP$FG","\\\\d{5,9}","[23567]\\\\d{5,7}|[489]\\\\d{6,8}",[["(2)(\\\\d)(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4","2",null],["(2)(\\\\d{3})(\\\\d{3,4})","$1 $2 $3","2",null],["(\\\\d{3})(\\\\d{4})","$1 $2","43[124-7]|70[1-9]",null],["(\\\\d{3})(\\\\d{3})(\\\\d{2})","$1 $2 $3","43[124-7]|70[1-9]",null],["(\\\\d{3})(\\\\d{2})(\\\\d{3})","$1 $2 $3","[78]00",null],["(\\\\d{3})(\\\\d{3})(\\\\d{3})","$1 $2 $3","999",null],["(\\\\d{2})(\\\\d{3})(\\\\d{2,3})","$1 $2 $3","[356]|4[124-7]|7[1-9]|8[1-6]|9[1-7]",null],["(\\\\d{2})(\\\\d{3})(\\\\d{3,4})","$1 $2 $3","48|8[7-9]|9[08]",null]]]',
"682": '["CK","00",null,null,null,null,"\\\\d{5}","[2-8]\\\\d{4}",[["(\\\\d{2})(\\\\d{3})","$1 $2",null,null]]]',
"852": '["HK","00(?:[126-9]|30|5[09])?",null,null,null,null,"\\\\d{5,11}","[235-7]\\\\d{7}|8\\\\d{7,8}|9\\\\d{4,10}",[["(\\\\d{4})(\\\\d{4})","$1 $2","[235-7]|[89](?:0[1-9]|[1-9])",null],["(800)(\\\\d{3})(\\\\d{3})","$1 $2 $3","800",null],["(900)(\\\\d{2})(\\\\d{3})(\\\\d{3})","$1 $2 $3 $4","900",null],["(900)(\\\\d{2,5})","$1 $2","900",null]]]',
"998": '["UZ","810","8",null,null,"$NP $FG","\\\\d{7,9}","[679]\\\\d{8}",[["([679]\\\\d)(\\\\d{3})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4",null,null]]]',
"291": '["ER","00","0",null,null,"$NP$FG","\\\\d{6,7}","[178]\\\\d{6}",[["(\\\\d)(\\\\d{3})(\\\\d{3})","$1 $2 $3",null,null]]]',
"95": '["MM","00","0",null,null,"$NP$FG","\\\\d{5,10}","[1478]\\\\d{5,7}|[256]\\\\d{5,8}|9(?:[279]\\\\d{0,2}|[58]|[34]\\\\d{1,2}|6\\\\d?)\\\\d{6}",[["(\\\\d)(\\\\d{3})(\\\\d{3,4})","$1 $2 $3","1|2[245]",null],["(2)(\\\\d{4})(\\\\d{4})","$1 $2 $3","251",null],["(\\\\d)(\\\\d{2})(\\\\d{3})","$1 $2 $3","16|2",null],["(\\\\d{2})(\\\\d{3})(\\\\d{3,4})","$1 $2 $3","67|81",null],["(\\\\d{2})(\\\\d{2})(\\\\d{3,4})","$1 $2 $3","[4-8]",null],["(9)(\\\\d{3})(\\\\d{4,6})","$1 $2 $3","9(?:2[0-4]|[35-9]|4[137-9])",null],["(9)([34]\\\\d{4})(\\\\d{4})","$1 $2 $3","9(?:3[0-36]|4[0-57-9])",null],["(9)(\\\\d{3})(\\\\d{3})(\\\\d{3})","$1 $2 $3 $4","92[56]",null],["(9)(\\\\d{3})(\\\\d{3})(\\\\d{2})","$1 $2 $3 $4","93",null]]]',
"266": '["LS","00",null,null,null,null,"\\\\d{8}","[2568]\\\\d{7}",[["(\\\\d{4})(\\\\d{4})","$1 $2",null,null]]]',
"245": '["GW","00",null,null,null,null,"\\\\d{7,9}","(?:4(?:0\\\\d{5}|4\\\\d{7})|9\\\\d{8})",[["(\\\\d{3})(\\\\d{3})(\\\\d{3})","$1 $2 $3","44|9[567]",null],["(\\\\d{3})(\\\\d{4})","$1 $2","40",null]]]',
"374": '["AM","00","0",null,null,"($NP$FG)","\\\\d{5,8}","[1-9]\\\\d{7}",[["(\\\\d{2})(\\\\d{6})","$1 $2","1|47",null],["(\\\\d{2})(\\\\d{6})","$1 $2","4[1349]|[5-7]|9[1-9]","$NP$FG"],["(\\\\d{3})(\\\\d{5})","$1 $2","[23]",null],["(\\\\d{3})(\\\\d{2})(\\\\d{3})","$1 $2 $3","8|90","$NP $FG"]]]',
"61": ['["AU","(?:14(?:1[14]|34|4[17]|[56]6|7[47]|88))?001[14-689]","0",null,null,null,"\\\\d{6,10}","1\\\\d{4,9}|[2-578]\\\\d{8}",[["([2378])(\\\\d{4})(\\\\d{4})","$1 $2 $3","[2378]","($NP$FG)"],["(\\\\d{3})(\\\\d{3})(\\\\d{3})","$1 $2 $3","[45]|14","$NP$FG"],["(16)(\\\\d{3,4})","$1 $2","16","$NP$FG"],["(16)(\\\\d{3})(\\\\d{2,4})","$1 $2 $3","16","$NP$FG"],["(1[389]\\\\d{2})(\\\\d{3})(\\\\d{3})","$1 $2 $3","1(?:[38]0|90)","$FG"],["(180)(2\\\\d{3})","$1 $2","180","$FG"],["(19\\\\d)(\\\\d{3})","$1 $2","19[13]","$FG"],["(19\\\\d{2})(\\\\d{4})","$1 $2","19[679]","$FG"],["(13)(\\\\d{2})(\\\\d{2})","$1 $2 $3","13[1-9]","$FG"]]]','["CC","(?:14(?:1[14]|34|4[17]|[56]6|7[47]|88))?001[14-689]","0",null,null,null,"\\\\d{6,10}","[1458]\\\\d{5,9}"]','["CX","(?:14(?:1[14]|34|4[17]|[56]6|7[47]|88))?001[14-689]","0",null,null,null,"\\\\d{6,10}","[1458]\\\\d{5,9}"]'],
"500": '["FK","00",null,null,null,null,"\\\\d{5}","[2-7]\\\\d{4}"]',
"261": '["MG","00","0",null,null,"$NP$FG","\\\\d{7,9}","[23]\\\\d{8}",[["([23]\\\\d)(\\\\d{2})(\\\\d{3})(\\\\d{2})","$1 $2 $3 $4",null,null]]]',
"92": '["PK","00","0",null,null,"($NP$FG)","\\\\d{6,12}","1\\\\d{8}|[2-8]\\\\d{5,11}|9(?:[013-9]\\\\d{4,9}|2\\\\d(?:111\\\\d{6}|\\\\d{3,7}))",[["(\\\\d{2})(111)(\\\\d{3})(\\\\d{3})","$1 $2 $3 $4","(?:2[125]|4[0-246-9]|5[1-35-7]|6[1-8]|7[14]|8[16]|91)1",null],["(\\\\d{3})(111)(\\\\d{3})(\\\\d{3})","$1 $2 $3 $4","2[349]|45|54|60|72|8[2-5]|9[2-9]",null],["(\\\\d{2})(\\\\d{7,8})","$1 $2","(?:2[125]|4[0-246-9]|5[1-35-7]|6[1-8]|7[14]|8[16]|91)[2-9]",null],["(\\\\d{3})(\\\\d{6,7})","$1 $2","2[349]|45|54|60|72|8[2-5]|9[2-9]",null],["(3\\\\d{2})(\\\\d{7})","$1 $2","3","$NP$FG"],["([15]\\\\d{3})(\\\\d{5,6})","$1 $2","58[12]|1",null],["(586\\\\d{2})(\\\\d{5})","$1 $2","586",null],["([89]00)(\\\\d{3})(\\\\d{2})","$1 $2 $3","[89]00","$NP$FG"]]]',
"234": '["NG","009","0",null,null,"$NP$FG","\\\\d{5,14}","[1-6]\\\\d{5,8}|9\\\\d{5,9}|[78]\\\\d{5,13}",[["(\\\\d)(\\\\d{3})(\\\\d{3,4})","$1 $2 $3","[12]|9(?:0[3-9]|[1-9])",null],["(\\\\d{2})(\\\\d{3})(\\\\d{2,3})","$1 $2 $3","[3-6]|7(?:[1-79]|0[1-9])|8[2-9]",null],["(\\\\d{3})(\\\\d{3})(\\\\d{3,4})","$1 $2 $3","70|8[01]|90[235-9]",null],["([78]00)(\\\\d{4})(\\\\d{4,5})","$1 $2 $3","[78]00",null],["([78]00)(\\\\d{5})(\\\\d{5,6})","$1 $2 $3","[78]00",null],["(78)(\\\\d{2})(\\\\d{3})","$1 $2 $3","78",null]]]',
"350": '["GI","00",null,null,null,null,"\\\\d{8}","[2568]\\\\d{7}",[["(\\\\d{3})(\\\\d{5})","$1 $2","2",null]]]',
"45": '["DK","00",null,null,null,null,"\\\\d{8}","[2-9]\\\\d{7}",[["(\\\\d{2})(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4",null,null]]]',
"963": '["SY","00","0",null,null,"$NP$FG","\\\\d{6,9}","[1-59]\\\\d{7,8}",[["(\\\\d{2})(\\\\d{3})(\\\\d{3,4})","$1 $2 $3","[1-5]",null],["(9\\\\d{2})(\\\\d{3})(\\\\d{3})","$1 $2 $3","9",null]]]',
"226": '["BF","00",null,null,null,null,"\\\\d{8}","[25-7]\\\\d{7}",[["(\\\\d{2})(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4",null,null]]]',
"974": '["QA","00",null,null,null,null,"\\\\d{7,8}","[2-8]\\\\d{6,7}",[["([28]\\\\d{2})(\\\\d{4})","$1 $2","[28]",null],["([3-7]\\\\d{3})(\\\\d{4})","$1 $2","[3-7]",null]]]',
"218": '["LY","00","0",null,null,"$NP$FG","\\\\d{7,9}","[25679]\\\\d{8}",[["([25679]\\\\d)(\\\\d{7})","$1-$2",null,null]]]',
"51": '["PE","19(?:1[124]|77|90)00","0",null,null,"($NP$FG)","\\\\d{6,9}","[14-9]\\\\d{7,8}",[["(1)(\\\\d{7})","$1 $2","1",null],["([4-8]\\\\d)(\\\\d{6})","$1 $2","[4-7]|8[2-4]",null],["(\\\\d{3})(\\\\d{5})","$1 $2","80",null],["(9\\\\d{2})(\\\\d{3})(\\\\d{3})","$1 $2 $3","9","$FG"]]]',
"62": '["ID","0(?:0[1789]|10(?:00|1[67]))","0",null,null,"$NP$FG","\\\\d{5,12}","(?:[1-79]\\\\d{6,10}|8\\\\d{7,11})",[["(\\\\d{2})(\\\\d{5,8})","$1 $2","2[124]|[36]1","($NP$FG)"],["(\\\\d{3})(\\\\d{5,8})","$1 $2","[4579]|2[035-9]|[36][02-9]","($NP$FG)"],["(8\\\\d{2})(\\\\d{3,4})(\\\\d{3})","$1-$2-$3","8[1-35-9]",null],["(8\\\\d{2})(\\\\d{4})(\\\\d{4,5})","$1-$2-$3","8[1-35-9]",null],["(1)(500)(\\\\d{3})","$1 $2 $3","15","$FG"],["(177)(\\\\d{6,8})","$1 $2","17",null],["(800)(\\\\d{5,7})","$1 $2","800",null],["(804)(\\\\d{3})(\\\\d{4})","$1 $2 $3","804",null],["(80\\\\d)(\\\\d)(\\\\d{3})(\\\\d{3})","$1 $2 $3 $4","80[79]",null]]]',
"298": '["FO","00",null,"(10(?:01|[12]0|88))",null,null,"\\\\d{6}","[2-9]\\\\d{5}",[["(\\\\d{6})","$1",null,null]]]',
"381": '["RS","00","0",null,null,"$NP$FG","\\\\d{5,12}","[126-9]\\\\d{4,11}|3(?:[0-79]\\\\d{3,10}|8[2-9]\\\\d{2,9})",[["([23]\\\\d{2})(\\\\d{4,9})","$1 $2","(?:2[389]|39)0",null],["([1-3]\\\\d)(\\\\d{5,10})","$1 $2","1|2(?:[0-24-7]|[389][1-9])|3(?:[0-8]|9[1-9])",null],["(6\\\\d)(\\\\d{6,8})","$1 $2","6",null],["([89]\\\\d{2})(\\\\d{3,9})","$1 $2","[89]",null],["(7[26])(\\\\d{4,9})","$1 $2","7[26]",null],["(7[08]\\\\d)(\\\\d{4,9})","$1 $2","7[08]",null]]]',
"975": '["BT","00",null,null,null,null,"\\\\d{6,8}","[1-8]\\\\d{6,7}",[["(\\\\d{2})(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4","1|77",null],["([2-8])(\\\\d{3})(\\\\d{3})","$1 $2 $3","[2-68]|7[246]",null]]]',
"34": '["ES","00",null,null,null,null,"\\\\d{9}","[5-9]\\\\d{8}",[["([89]00)(\\\\d{3})(\\\\d{3})","$1 $2 $3","[89]00",null],["([5-9]\\\\d{2})(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4","[568]|[79][0-8]",null]]]',
"881": '["001",null,null,null,null,null,"\\\\d{9}","[67]\\\\d{8}",[["(\\\\d)(\\\\d{3})(\\\\d{5})","$1 $2 $3","[67]",null]]]',
"855": '["KH","00[14-9]","0",null,null,null,"\\\\d{6,10}","[1-9]\\\\d{7,9}",[["(\\\\d{2})(\\\\d{3})(\\\\d{3,4})","$1 $2 $3","1\\\\d[1-9]|[2-9]","$NP$FG"],["(1[89]00)(\\\\d{3})(\\\\d{3})","$1 $2 $3","1[89]0",null]]]',
"420": '["CZ","00",null,null,null,null,"\\\\d{9,12}","[2-8]\\\\d{8}|9\\\\d{8,11}",[["([2-9]\\\\d{2})(\\\\d{3})(\\\\d{3})","$1 $2 $3","[2-8]|9[015-7]",null],["(96\\\\d)(\\\\d{3})(\\\\d{3})(\\\\d{3})","$1 $2 $3 $4","96",null],["(9\\\\d)(\\\\d{3})(\\\\d{3})(\\\\d{3})","$1 $2 $3 $4","9[36]",null]]]',
"216": '["TN","00",null,null,null,null,"\\\\d{8}","[2-57-9]\\\\d{7}",[["(\\\\d{2})(\\\\d{3})(\\\\d{3})","$1 $2 $3",null,null]]]',
"673": '["BN","00",null,null,null,null,"\\\\d{7}","[2-578]\\\\d{6}",[["([2-578]\\\\d{2})(\\\\d{4})","$1 $2",null,null]]]',
"290": ['["SH","00",null,null,null,null,"\\\\d{4,5}","[256]\\\\d{4}"]','["TA","00",null,null,null,null,"\\\\d{4}","8\\\\d{3}"]'],
"882": '["001",null,null,null,null,null,"\\\\d{7,12}","[13]\\\\d{6,11}",[["(\\\\d{2})(\\\\d{4})(\\\\d{3})","$1 $2 $3","3[23]",null],["(\\\\d{2})(\\\\d{5})","$1 $2","16|342",null],["(\\\\d{2})(\\\\d{4})(\\\\d{4})","$1 $2 $3","34[57]",null],["(\\\\d{3})(\\\\d{4})(\\\\d{4})","$1 $2 $3","348",null],["(\\\\d{2})(\\\\d{2})(\\\\d{4})","$1 $2 $3","1",null],["(\\\\d{2})(\\\\d{3,4})(\\\\d{4})","$1 $2 $3","16",null],["(\\\\d{2})(\\\\d{4,5})(\\\\d{5})","$1 $2 $3","16|39",null]]]',
"267": '["BW","00",null,null,null,null,"\\\\d{7,8}","[2-79]\\\\d{6,7}",[["(\\\\d{3})(\\\\d{4})","$1 $2","[2-6]",null],["(7\\\\d)(\\\\d{3})(\\\\d{3})","$1 $2 $3","7",null],["(90)(\\\\d{5})","$1 $2","9",null]]]',
"94": '["LK","00","0",null,null,"$NP$FG","\\\\d{7,9}","[1-9]\\\\d{8}",[["(\\\\d{2})(\\\\d{1})(\\\\d{6})","$1 $2 $3","[1-689]",null],["(\\\\d{2})(\\\\d{3})(\\\\d{4})","$1 $2 $3","7",null]]]',
"356": '["MT","00",null,null,null,null,"\\\\d{8}","[2357-9]\\\\d{7}",[["(\\\\d{4})(\\\\d{4})","$1 $2",null,null]]]',
"375": '["BY","810","8","8?0?",null,null,"\\\\d{5,11}","[1-4]\\\\d{8}|800\\\\d{3,7}|[89]\\\\d{9,10}",[["(\\\\d{2})(\\\\d{3})(\\\\d{2})(\\\\d{2})","$1 $2-$3-$4","17[0-3589]|2[4-9]|[34]","$NP 0$FG"],["(\\\\d{3})(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2-$3-$4","1(?:5[24]|6[235]|7[467])|2(?:1[246]|2[25]|3[26])","$NP 0$FG"],["(\\\\d{4})(\\\\d{2})(\\\\d{3})","$1 $2-$3","1(?:5[169]|6[3-5]|7[179])|2(?:1[35]|2[34]|3[3-5])","$NP 0$FG"],["([89]\\\\d{2})(\\\\d{3})(\\\\d{4})","$1 $2 $3","8[01]|9","$NP $FG"],["(82\\\\d)(\\\\d{4})(\\\\d{4})","$1 $2 $3","82","$NP $FG"],["(800)(\\\\d{3})","$1 $2","800","$NP $FG"],["(800)(\\\\d{2})(\\\\d{2,4})","$1 $2 $3","800","$NP $FG"]]]',
"690": '["TK","00",null,null,null,null,"\\\\d{4,7}","[2-47]\\\\d{3,6}"]',
"507": '["PA","00",null,null,null,null,"\\\\d{7,8}","[1-9]\\\\d{6,7}",[["(\\\\d{3})(\\\\d{4})","$1-$2","[1-57-9]",null],["(\\\\d{4})(\\\\d{4})","$1-$2","6",null]]]',
"692": '["MH","011","1",null,null,null,"\\\\d{7}","[2-6]\\\\d{6}",[["(\\\\d{3})(\\\\d{4})","$1-$2",null,null]]]',
"250": '["RW","00","0",null,null,null,"\\\\d{8,9}","[027-9]\\\\d{7,8}",[["(2\\\\d{2})(\\\\d{3})(\\\\d{3})","$1 $2 $3","2","$FG"],["([7-9]\\\\d{2})(\\\\d{3})(\\\\d{3})","$1 $2 $3","[7-9]","$NP$FG"],["(0\\\\d)(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4","0",null]]]',
"81": '["JP","010","0",null,null,"$NP$FG","\\\\d{8,17}","[1-9]\\\\d{8,9}|00(?:[36]\\\\d{7,14}|7\\\\d{5,7}|8\\\\d{7})",[["(\\\\d{3})(\\\\d{3})(\\\\d{3})","$1-$2-$3","(?:12|57|99)0",null],["(\\\\d{3})(\\\\d{3})(\\\\d{4})","$1-$2-$3","800",null],["(\\\\d{4})(\\\\d{4})","$1-$2","0077","$FG","NA"],["(\\\\d{4})(\\\\d{2})(\\\\d{3,4})","$1-$2-$3","0077","$FG","NA"],["(\\\\d{4})(\\\\d{2})(\\\\d{4})","$1-$2-$3","0088","$FG","NA"],["(\\\\d{4})(\\\\d{3})(\\\\d{3,4})","$1-$2-$3","00(?:37|66)","$FG","NA"],["(\\\\d{4})(\\\\d{4})(\\\\d{4,5})","$1-$2-$3","00(?:37|66)","$FG","NA"],["(\\\\d{4})(\\\\d{5})(\\\\d{5,6})","$1-$2-$3","00(?:37|66)","$FG","NA"],["(\\\\d{4})(\\\\d{6})(\\\\d{6,7})","$1-$2-$3","00(?:37|66)","$FG","NA"],["(\\\\d{2})(\\\\d{4})(\\\\d{4})","$1-$2-$3","[2579]0|80[1-9]",null],["(\\\\d{4})(\\\\d)(\\\\d{4})","$1-$2-$3","1(?:26|3[79]|4[56]|5[4-68]|6[3-5])|5(?:76|97)|499|746|8(?:3[89]|63|47|51)|9(?:49|80|9[16])",null],["(\\\\d{3})(\\\\d{2})(\\\\d{4})","$1-$2-$3","1(?:2[3-6]|3[3-9]|4[2-6]|5[2-8]|[68][2-7]|7[2-689]|9[1-578])|2(?:2[03-689]|3[3-58]|4[0-468]|5[04-8]|6[013-8]|7[06-9]|8[02-57-9]|9[13])|4(?:2[28]|3[689]|6[035-7]|7[05689]|80|9[3-5])|5(?:3[1-36-9]|4[4578]|5[013-8]|6[1-9]|7[2-8]|8[14-7]|9[4-9])|7(?:2[15]|3[5-9]|4[02-9]|6[135-8]|7[0-4689]|9[014-9])|8(?:2[49]|3[3-8]|4[5-8]|5[2-9]|6[35-9]|7[579]|8[03-579]|9[2-8])|9(?:[23]0|4[02-46-9]|5[024-79]|6[4-9]|7[2-47-9]|8[02-7]|9[3-7])",null],["(\\\\d{2})(\\\\d{3})(\\\\d{4})","$1-$2-$3","1|2(?:2[37]|5[5-9]|64|78|8[39]|91)|4(?:2[2689]|64|7[347])|5(?:[2-589]|39)|60|8(?:[46-9]|3[279]|2[124589])|9(?:[235-8]|93)",null],["(\\\\d{3})(\\\\d{2})(\\\\d{4})","$1-$2-$3","2(?:9[14-79]|74|[34]7|[56]9)|82|993",null],["(\\\\d)(\\\\d{4})(\\\\d{4})","$1-$2-$3","3|4(?:2[09]|7[01])|6[1-9]",null],["(\\\\d{2})(\\\\d{3})(\\\\d{4})","$1-$2-$3","[2479][1-9]",null]]]',
"237": '["CM","00",null,null,null,null,"\\\\d{8,9}","[2368]\\\\d{7,8}",[["([26])(\\\\d{2})(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4 $5","[26]",null],["(\\\\d{2})(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4","[23]|88",null],["(800)(\\\\d{2})(\\\\d{3})","$1 $2 $3","80",null]]]',
"351": '["PT","00",null,null,null,null,"\\\\d{9}","[2-46-9]\\\\d{8}",[["(2\\\\d)(\\\\d{3})(\\\\d{4})","$1 $2 $3","2[12]",null],["([2-46-9]\\\\d{2})(\\\\d{3})(\\\\d{3})","$1 $2 $3","2[3-9]|[346-9]",null]]]',
"246": '["IO","00",null,null,null,null,"\\\\d{7}","3\\\\d{6}",[["(\\\\d{3})(\\\\d{4})","$1 $2",null,null]]]',
"227": '["NE","00",null,null,null,null,"\\\\d{8}","[0289]\\\\d{7}",[["(\\\\d{2})(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4","[289]|09",null],["(08)(\\\\d{3})(\\\\d{3})","$1 $2 $3","08",null]]]',
"27": '["ZA","00","0",null,null,"$NP$FG","\\\\d{5,9}","[1-79]\\\\d{8}|8\\\\d{4,8}",[["(860)(\\\\d{3})(\\\\d{3})","$1 $2 $3","860",null],["(\\\\d{2})(\\\\d{3,4})","$1 $2","8[1-4]",null],["(\\\\d{2})(\\\\d{3})(\\\\d{2,3})","$1 $2 $3","8[1-4]",null],["(\\\\d{2})(\\\\d{3})(\\\\d{4})","$1 $2 $3","[1-79]|8(?:[0-57]|6[1-9])",null]]]',
"962": '["JO","00","0",null,null,"$NP$FG","\\\\d{8,9}","[235-9]\\\\d{7,8}",[["(\\\\d)(\\\\d{3})(\\\\d{4})","$1 $2 $3","[2356]|87","($NP$FG)"],["(7)(\\\\d{4})(\\\\d{4})","$1 $2 $3","7[457-9]",null],["(\\\\d{3})(\\\\d{5,6})","$1 $2","70|8[0158]|9",null]]]',
"387": '["BA","00","0",null,null,"$NP$FG","\\\\d{6,9}","[3-9]\\\\d{7,8}",[["(\\\\d{2})(\\\\d{3})(\\\\d{3})","$1 $2-$3","[3-5]",null],["(\\\\d{2})(\\\\d{3})(\\\\d{3})","$1 $2 $3","6[1-356]|[7-9]",null],["(\\\\d{2})(\\\\d{2})(\\\\d{2})(\\\\d{3})","$1 $2 $3 $4","6[047]",null]]]',
"33": '["FR","00","0",null,null,"$NP$FG","\\\\d{9}","[1-9]\\\\d{8}",[["([1-79])(\\\\d{2})(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4 $5","[1-79]",null],["(1\\\\d{2})(\\\\d{3})","$1 $2","11","$FG","NA"],["(8\\\\d{2})(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4","8","$NP $FG"]]]',
"972": '["IL","0(?:0|1[2-9])","0",null,null,"$FG","\\\\d{4,12}","1\\\\d{6,11}|[2-589]\\\\d{3}(?:\\\\d{3,6})?|6\\\\d{3}|7\\\\d{6,9}",[["([2-489])(\\\\d{3})(\\\\d{4})","$1-$2-$3","[2-489]","$NP$FG"],["([57]\\\\d)(\\\\d{3})(\\\\d{4})","$1-$2-$3","[57]","$NP$FG"],["(153)(\\\\d{1,2})(\\\\d{3})(\\\\d{4})","$1 $2 $3 $4","153",null],["(1)([7-9]\\\\d{2})(\\\\d{3})(\\\\d{3})","$1-$2-$3-$4","1[7-9]",null],["(1255)(\\\\d{3})","$1-$2","125",null],["(1200)(\\\\d{3})(\\\\d{3})","$1-$2-$3","120",null],["(1212)(\\\\d{2})(\\\\d{2})","$1-$2-$3","121",null],["(1599)(\\\\d{6})","$1-$2","15",null],["(\\\\d{4})","*$1","[2-689]",null]]]',
"248": '["SC","0(?:[02]|10?)",null,null,null,null,"\\\\d{6,7}","[24689]\\\\d{5,6}",[["(\\\\d)(\\\\d{3})(\\\\d{3})","$1 $2 $3","[246]",null]]]',
"297": '["AW","00",null,null,null,null,"\\\\d{7}","[25-9]\\\\d{6}",[["(\\\\d{3})(\\\\d{4})","$1 $2",null,null]]]',
"421": '["SK","00","0",null,null,"$NP$FG","\\\\d{6,9}","(?:[2-68]\\\\d{5,8}|9\\\\d{6,8})",[["(2)(1[67])(\\\\d{3,4})","$1 $2 $3","21[67]",null],["([3-5]\\\\d)(1[67])(\\\\d{2,3})","$1 $2 $3","[3-5]",null],["(2)(\\\\d{3})(\\\\d{3})(\\\\d{2})","$1/$2 $3 $4","2",null],["([3-5]\\\\d)(\\\\d{3})(\\\\d{2})(\\\\d{2})","$1/$2 $3 $4","[3-5]",null],["([689]\\\\d{2})(\\\\d{3})(\\\\d{3})","$1 $2 $3","[689]",null],["(9090)(\\\\d{3})","$1 $2","9090",null]]]',
"672": '["NF","00",null,null,null,null,"\\\\d{5,6}","[13]\\\\d{5}",[["(\\\\d{2})(\\\\d{4})","$1 $2","1",null],["(\\\\d)(\\\\d{5})","$1 $2","3",null]]]',
"870": '["001",null,null,null,null,null,"\\\\d{9}","[35-7]\\\\d{8}",[["(\\\\d{3})(\\\\d{3})(\\\\d{3})","$1 $2 $3",null,null]]]',
"883": '["001",null,null,null,null,null,"\\\\d{9}(?:\\\\d{3})?","51\\\\d{7}(?:\\\\d{3})?",[["(\\\\d{3})(\\\\d{3})(\\\\d{3})","$1 $2 $3","510",null],["(\\\\d{3})(\\\\d{3})(\\\\d{3})(\\\\d{3})","$1 $2 $3 $4","510",null],["(\\\\d{4})(\\\\d{4})(\\\\d{4})","$1 $2 $3","51[13]",null]]]',
"264": '["NA","00","0",null,null,"$NP$FG","\\\\d{8,9}","[68]\\\\d{7,8}",[["(8\\\\d)(\\\\d{3})(\\\\d{4})","$1 $2 $3","8[1235]",null],["(6\\\\d)(\\\\d{3})(\\\\d{3,4})","$1 $2 $3","6",null],["(88)(\\\\d{3})(\\\\d{3})","$1 $2 $3","88",null],["(870)(\\\\d{3})(\\\\d{3})","$1 $2 $3","870",null]]]',
"878": '["001",null,null,null,null,null,"\\\\d{12}","1\\\\d{11}",[["(\\\\d{2})(\\\\d{5})(\\\\d{5})","$1 $2 $3",null,null]]]',
"239": '["ST","00",null,null,null,null,"\\\\d{7}","[29]\\\\d{6}",[["(\\\\d{3})(\\\\d{4})","$1 $2",null,null]]]',
"357": '["CY","00",null,null,null,null,"\\\\d{8}","[257-9]\\\\d{7}",[["(\\\\d{2})(\\\\d{6})","$1 $2",null,null]]]',
"240": '["GQ","00",null,null,null,null,"\\\\d{9}","[23589]\\\\d{8}",[["(\\\\d{3})(\\\\d{3})(\\\\d{3})","$1 $2 $3","[235]",null],["(\\\\d{3})(\\\\d{6})","$1 $2","[89]",null]]]',
"506": '["CR","00",null,"(19(?:0[012468]|1[09]|20|66|77|99))",null,null,"\\\\d{8,10}","[24-9]\\\\d{7,9}",[["(\\\\d{4})(\\\\d{4})","$1 $2","[24-7]|8[3-9]",null],["(\\\\d{3})(\\\\d{3})(\\\\d{4})","$1-$2-$3","[89]0",null]]]',
"86": '["CN","(1(?:[129]\\\\d{3}|79\\\\d{2}))?00","0","(1(?:[129]\\\\d{3}|79\\\\d{2}))|0",null,null,"\\\\d{4,12}","[1-7]\\\\d{6,11}|8[0-357-9]\\\\d{6,9}|9\\\\d{7,10}",[["(80\\\\d{2})(\\\\d{4})","$1 $2","80[2678]","$NP$FG"],["([48]00)(\\\\d{3})(\\\\d{4})","$1 $2 $3","[48]00",null],["(\\\\d{5,6})","$1","100|95",null,"NA"],["(\\\\d{2})(\\\\d{5,6})","$1 $2","(?:10|2\\\\d)[19]","$NP$FG"],["(\\\\d{3})(\\\\d{5,6})","$1 $2","[3-9]","$NP$FG"],["(\\\\d{3,4})(\\\\d{4})","$1 $2","[2-9]",null,"NA"],["(21)(\\\\d{4})(\\\\d{4,6})","$1 $2 $3","21","$NP$FG"],["([12]\\\\d)(\\\\d{4})(\\\\d{4})","$1 $2 $3","10[1-9]|2[02-9]","$NP$FG"],["(\\\\d{3})(\\\\d{3})(\\\\d{4})","$1 $2 $3","3(?:1[02-9]|35|49|5|7[02-68]|9[1-68])|4(?:1[02-9]|2[179]|[35][2-9]|6[4789]|7\\\\d|8[23])|5(?:3[03-9]|4[36]|5[02-9]|6[1-46]|7[028]|80|9[2-46-9])|6(?:3[1-5]|6[0238]|9[12])|7(?:01|[1579]|2[248]|3[04-9]|4[3-6]|6[2368])|8(?:1[236-8]|2[5-7]|3|5[1-9]|7[02-9]|8[3678]|9[1-7])|9(?:0[1-3689]|1[1-79]|[379]|4[13]|5[1-5])","$NP$FG"],["(\\\\d{3})(\\\\d{4})(\\\\d{4})","$1 $2 $3","3(?:11|7[179])|4(?:[15]1|3[1-35])|5(?:1|2[37]|3[12]|51|7[13-79]|9[15])|7(?:31|5[457]|6[09]|91)|8(?:[57]1|98)","$NP$FG"],["(\\\\d{4})(\\\\d{3})(\\\\d{4})","$1 $2 $3","807","$NP$FG"],["(\\\\d{3})(\\\\d{4})(\\\\d{4})","$1 $2 $3","1[3-578]",null],["(10800)(\\\\d{3})(\\\\d{4})","$1 $2 $3","108",null],["(\\\\d{3})(\\\\d{7,8})","$1 $2","950",null]]]',
"257": '["BI","00",null,null,null,null,"\\\\d{8}","[267]\\\\d{7}",[["(\\\\d{2})(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4",null,null]]]',
"683": '["NU","00",null,null,null,null,"\\\\d{4}","[1-5]\\\\d{3}"]',
"43": '["AT","00","0",null,null,"$NP$FG","\\\\d{3,13}","[1-9]\\\\d{3,12}",[["(116\\\\d{3})","$1","116","$FG"],["(1)(\\\\d{3,12})","$1 $2","1",null],["(5\\\\d)(\\\\d{3,5})","$1 $2","5[079]",null],["(5\\\\d)(\\\\d{3})(\\\\d{3,4})","$1 $2 $3","5[079]",null],["(5\\\\d)(\\\\d{4})(\\\\d{4,7})","$1 $2 $3","5[079]",null],["(\\\\d{3})(\\\\d{3,10})","$1 $2","316|46|51|732|6(?:5[0-3579]|[6-9])|7(?:[28]0)|[89]",null],["(\\\\d{4})(\\\\d{3,9})","$1 $2","2|3(?:1[1-578]|[3-8])|4[2378]|5[2-6]|6(?:[12]|4[1-9]|5[468])|7(?:2[1-8]|35|4[1-8]|[5-79])",null]]]',
"247": '["AC","00",null,null,null,null,"\\\\d{5,6}","[46]\\\\d{4}|[01589]\\\\d{5}"]',
"675": '["PG","00",null,null,null,null,"\\\\d{7,8}","[1-9]\\\\d{6,7}",[["(\\\\d{3})(\\\\d{4})","$1 $2","[13-689]|27",null],["(\\\\d{4})(\\\\d{4})","$1 $2","20|7",null]]]',
"376": '["AD","00",null,null,null,null,"\\\\d{6,9}","[16]\\\\d{5,8}|[37-9]\\\\d{5}",[["(\\\\d{3})(\\\\d{3})","$1 $2","[137-9]|6[0-8]",null],["(\\\\d{4})(\\\\d{4})","$1 $2","180",null],["(\\\\d{3})(\\\\d{3})(\\\\d{3})","$1 $2 $3","690",null]]]',
"63": '["PH","00","0",null,null,null,"\\\\d{5,13}","2\\\\d{5,7}|[3-9]\\\\d{7,9}|1800\\\\d{7,9}",[["(2)(\\\\d{3})(\\\\d{4})","$1 $2 $3","2","($NP$FG)"],["(2)(\\\\d{5})","$1 $2","2","($NP$FG)"],["(\\\\d{4})(\\\\d{4,6})","$1 $2","3(?:23|39|46)|4(?:2[3-6]|[35]9|4[26]|76)|5(?:22|44)|642|8(?:62|8[245])","($NP$FG)"],["(\\\\d{5})(\\\\d{4})","$1 $2","346|4(?:27|9[35])|883","($NP$FG)"],["([3-8]\\\\d)(\\\\d{3})(\\\\d{4})","$1 $2 $3","[3-8]","($NP$FG)"],["(\\\\d{3})(\\\\d{3})(\\\\d{4})","$1 $2 $3","81|9","$NP$FG"],["(1800)(\\\\d{3})(\\\\d{4})","$1 $2 $3","1",null],["(1800)(\\\\d{1,2})(\\\\d{3})(\\\\d{4})","$1 $2 $3 $4","1",null]]]',
"236": '["CF","00",null,null,null,null,"\\\\d{8}","[278]\\\\d{7}",[["(\\\\d{2})(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4",null,null]]]',
"590": ['["GP","00","0",null,null,"$NP$FG","\\\\d{9}","[56]\\\\d{8}",[["([56]90)(\\\\d{2})(\\\\d{4})","$1 $2-$3",null,null]]]','["BL","00","0",null,null,null,"\\\\d{9}","[56]\\\\d{8}"]','["MF","00","0",null,null,null,"\\\\d{9}","[56]\\\\d{8}"]'],
"53": '["CU","119","0",null,null,"($NP$FG)","\\\\d{4,8}","[2-57]\\\\d{5,7}",[["(\\\\d)(\\\\d{6,7})","$1 $2","7",null],["(\\\\d{2})(\\\\d{4,6})","$1 $2","[2-4]",null],["(\\\\d)(\\\\d{7})","$1 $2","5","$NP$FG"]]]',
"64": '["NZ","0(?:0|161)","0",null,null,"$NP$FG","\\\\d{7,11}","6[235-9]\\\\d{6}|[2-57-9]\\\\d{7,10}",[["([34679])(\\\\d{3})(\\\\d{4})","$1-$2 $3","[346]|7[2-57-9]|9[1-9]",null],["(24099)(\\\\d{3})","$1 $2","240",null],["(\\\\d{2})(\\\\d{3})(\\\\d{3})","$1 $2 $3","21",null],["(\\\\d{2})(\\\\d{3})(\\\\d{3,5})","$1 $2 $3","2(?:1[1-9]|[69]|7[0-35-9])|70|86",null],["(2\\\\d)(\\\\d{3,4})(\\\\d{4})","$1 $2 $3","2[028]",null],["(\\\\d{3})(\\\\d{3})(\\\\d{3,4})","$1 $2 $3","2(?:10|74)|5|[89]0",null]]]',
"965": '["KW","00",null,null,null,null,"\\\\d{7,8}","[12569]\\\\d{6,7}",[["(\\\\d{4})(\\\\d{3,4})","$1 $2","[16]|2(?:[0-35-9]|4[0-35-9])|9[024-9]|52[25]",null],["(\\\\d{3})(\\\\d{5})","$1 $2","244|5(?:[015]|66)",null]]]',
"224": '["GN","00",null,null,null,null,"\\\\d{8,9}","[367]\\\\d{7,8}",[["(\\\\d{2})(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4","3",null],["(\\\\d{3})(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4","[67]",null]]]',
"973": '["BH","00",null,null,null,null,"\\\\d{8}","[136-9]\\\\d{7}",[["(\\\\d{4})(\\\\d{4})","$1 $2",null,null]]]',
"32": '["BE","00","0",null,null,"$NP$FG","\\\\d{8,9}","[1-9]\\\\d{7,8}",[["(\\\\d{3})(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4","4[6-9]",null],["(\\\\d)(\\\\d{3})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4","[23]|4[23]|9[2-4]",null],["(\\\\d{2})(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4","[156]|7[018]|8(?:0[1-9]|[1-79])",null],["(\\\\d{3})(\\\\d{2})(\\\\d{3})","$1 $2 $3","(?:80|9)0",null]]]',
"249": '["SD","00","0",null,null,"$NP$FG","\\\\d{9}","[19]\\\\d{8}",[["(\\\\d{2})(\\\\d{3})(\\\\d{4})","$1 $2 $3",null,null]]]',
"678": '["VU","00",null,null,null,null,"\\\\d{5,7}","[2-57-9]\\\\d{4,6}",[["(\\\\d{3})(\\\\d{4})","$1 $2","[579]",null]]]',
"52": '["MX","0[09]","01","0[12]|04[45](\\\\d{10})","1$1","$NP $FG","\\\\d{7,11}","[1-9]\\\\d{9,10}",[["([358]\\\\d)(\\\\d{4})(\\\\d{4})","$1 $2 $3","33|55|81",null],["(\\\\d{3})(\\\\d{3})(\\\\d{4})","$1 $2 $3","[2467]|3[0-2457-9]|5[089]|8[02-9]|9[0-35-9]",null],["(1)([358]\\\\d)(\\\\d{4})(\\\\d{4})","044 $2 $3 $4","1(?:33|55|81)","$FG","$1 $2 $3 $4"],["(1)(\\\\d{3})(\\\\d{3})(\\\\d{4})","044 $2 $3 $4","1(?:[2467]|3[0-2457-9]|5[089]|8[2-9]|9[1-35-9])","$FG","$1 $2 $3 $4"]]]',
"968": '["OM","00",null,null,null,null,"\\\\d{7,9}","(?:5|[279]\\\\d)\\\\d{6}|800\\\\d{5,6}",[["(2\\\\d)(\\\\d{6})","$1 $2","2",null],["([79]\\\\d{3})(\\\\d{4})","$1 $2","[79]",null],["([58]00)(\\\\d{4,6})","$1 $2","[58]",null]]]',
"599": ['["CW","00",null,null,null,null,"\\\\d{7,8}","[169]\\\\d{6,7}",[["(\\\\d{3})(\\\\d{4})","$1 $2","[13-7]",null],["(9)(\\\\d{3})(\\\\d{4})","$1 $2 $3","9",null]]]','["BQ","00",null,null,null,null,"\\\\d{7}","[347]\\\\d{6}"]'],
"800": '["001",null,null,null,null,null,"\\\\d{8}","\\\\d{8}",[["(\\\\d{4})(\\\\d{4})","$1 $2",null,null]]]',
"386": '["SI","00","0",null,null,"$NP$FG","\\\\d{5,8}","[1-7]\\\\d{6,7}|[89]\\\\d{4,7}",[["(\\\\d)(\\\\d{3})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4","[12]|3[24-8]|4[24-8]|5[2-8]|7[3-8]","($NP$FG)"],["([3-7]\\\\d)(\\\\d{3})(\\\\d{3})","$1 $2 $3","[37][01]|4[0139]|51|6",null],["([89][09])(\\\\d{3,6})","$1 $2","[89][09]",null],["([58]\\\\d{2})(\\\\d{5})","$1 $2","59|8[1-3]",null]]]',
"679": '["FJ","0(?:0|52)",null,null,null,null,"\\\\d{7}(?:\\\\d{4})?","[35-9]\\\\d{6}|0\\\\d{10}",[["(\\\\d{3})(\\\\d{4})","$1 $2","[35-9]",null],["(\\\\d{4})(\\\\d{3})(\\\\d{4})","$1 $2 $3","0",null]]]',
"238": '["CV","0",null,null,null,null,"\\\\d{7}","[259]\\\\d{6}",[["(\\\\d{3})(\\\\d{2})(\\\\d{2})","$1 $2 $3",null,null]]]',
"691": '["FM","00",null,null,null,null,"\\\\d{7}","[39]\\\\d{6}",[["(\\\\d{3})(\\\\d{4})","$1 $2",null,null]]]',
"262": ['["RE","00","0",null,null,"$NP$FG","\\\\d{9}","[268]\\\\d{8}",[["([268]\\\\d{2})(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4",null,null]]]','["YT","00","0",null,null,"$NP$FG","\\\\d{9}","[268]\\\\d{8}"]'],
"241": '["GA","00",null,null,null,null,"\\\\d{7,8}","0?\\\\d{7}",[["(\\\\d)(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4","[2-7]","0$FG"],["(\\\\d{2})(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4","0",null]]]',
"370": '["LT","00","8","[08]",null,"($NP-$FG)","\\\\d{8}","[3-9]\\\\d{7}",[["([34]\\\\d)(\\\\d{6})","$1 $2","37|4(?:1|5[45]|6[2-4])",null],["([3-6]\\\\d{2})(\\\\d{5})","$1 $2","3[148]|4(?:[24]|6[09])|528|6",null],["([7-9]\\\\d{2})(\\\\d{2})(\\\\d{3})","$1 $2 $3","[7-9]","$NP $FG"],["(5)(2\\\\d{2})(\\\\d{4})","$1 $2 $3","52[0-79]",null]]]',
"256": '["UG","00[057]","0",null,null,"$NP$FG","\\\\d{5,9}","\\\\d{9}",[["(\\\\d{3})(\\\\d{6})","$1 $2","[7-9]|20(?:[013-8]|2[5-9])|4(?:6[45]|[7-9])",null],["(\\\\d{2})(\\\\d{7})","$1 $2","3|4(?:[1-5]|6[0-36-9])",null],["(2024)(\\\\d{5})","$1 $2","2024",null]]]',
"677": '["SB","0[01]",null,null,null,null,"\\\\d{5,7}","[1-9]\\\\d{4,6}",[["(\\\\d{2})(\\\\d{5})","$1 $2","[7-9]",null]]]',
"377": '["MC","00","0",null,null,"$NP$FG","\\\\d{8,9}","[34689]\\\\d{7,8}",[["(\\\\d{2})(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4","[39]","$FG"],["(\\\\d{2})(\\\\d{3})(\\\\d{3})","$1 $2 $3","4",null],["(6)(\\\\d{2})(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4 $5","6",null],["(\\\\d{3})(\\\\d{3})(\\\\d{2})","$1 $2 $3","8","$FG"]]]',
"382": '["ME","00","0",null,null,"$NP$FG","\\\\d{6,9}","[2-9]\\\\d{7,8}",[["(\\\\d{2})(\\\\d{3})(\\\\d{3})","$1 $2 $3","[2-57-9]|6[036-9]",null]]]',
"231": '["LR","00","0",null,null,"$NP$FG","\\\\d{7,9}","2\\\\d{7,8}|[378]\\\\d{8}|4\\\\d{6}|5\\\\d{6,8}",[["(2\\\\d)(\\\\d{3})(\\\\d{3})","$1 $2 $3","2",null],["([4-5])(\\\\d{3})(\\\\d{3})","$1 $2 $3","[45]",null],["(\\\\d{2})(\\\\d{3})(\\\\d{4})","$1 $2 $3","[23578]",null]]]',
"591": '["BO","00(1\\\\d)?","0","0(1\\\\d)?",null,null,"\\\\d{7,8}","[23467]\\\\d{7}",[["([234])(\\\\d{7})","$1 $2","[234]",null],["([67]\\\\d{7})","$1","[67]",null]]]',
"808": '["001",null,null,null,null,null,"\\\\d{8}","\\\\d{8}",[["(\\\\d{4})(\\\\d{4})","$1 $2",null,null]]]',
"964": '["IQ","00","0",null,null,"$NP$FG","\\\\d{6,10}","[1-7]\\\\d{7,9}",[["(1)(\\\\d{3})(\\\\d{4})","$1 $2 $3","1",null],["([2-6]\\\\d)(\\\\d{3})(\\\\d{3,4})","$1 $2 $3","[2-6]",null],["(7\\\\d{2})(\\\\d{3})(\\\\d{4})","$1 $2 $3","7",null]]]',
"225": '["CI","00",null,null,null,null,"\\\\d{8}","[02-8]\\\\d{7}",[["(\\\\d{2})(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4",null,null]]]',
"992": '["TJ","810","8",null,null,"$FG","\\\\d{3,9}","[3-57-9]\\\\d{8}",[["([349]\\\\d{2})(\\\\d{2})(\\\\d{4})","$1 $2 $3","[34]7|91[78]",null],["([457-9]\\\\d)(\\\\d{3})(\\\\d{4})","$1 $2 $3","4[148]|[578]|9(?:1[59]|[0235-9])",null],["(331700)(\\\\d)(\\\\d{2})","$1 $2 $3","331",null],["(\\\\d{4})(\\\\d)(\\\\d{4})","$1 $2 $3","3[1-5]",null]]]',
"55": '["BR","00(?:1[245]|2[1-35]|31|4[13]|[56]5|99)","0","(?:0|90)(?:(1[245]|2[135]|[34]1)(\\\\d{10,11}))?","$2",null,"\\\\d{8,11}","[1-46-9]\\\\d{7,10}|5(?:[0-4]\\\\d{7,9}|5(?:[2-8]\\\\d{7}|9\\\\d{7,8}))",[["(\\\\d{4})(\\\\d{4})","$1-$2","[2-9](?:[1-9]|0[1-9])","$FG","NA"],["(\\\\d{5})(\\\\d{4})","$1-$2","9(?:[1-9]|0[1-9])","$FG","NA"],["(\\\\d{3,5})","$1","1[125689]","$FG","NA"],["(\\\\d{2})(\\\\d{4})(\\\\d{4})","$1 $2-$3","[1-9][1-9]","($FG)"],["(\\\\d{2})(\\\\d{5})(\\\\d{4})","$1 $2-$3","(?:[14689][1-9]|2[12478]|3[1-578]|5[1-5]|7[13-579])9","($FG)"],["(\\\\d{4})(\\\\d{4})","$1-$2","(?:300|40(?:0|20))",null],["([3589]00)(\\\\d{2,3})(\\\\d{4})","$1 $2 $3","[3589]00","$NP$FG"]]]',
"674": '["NR","00",null,null,null,null,"\\\\d{7}","[458]\\\\d{6}",[["(\\\\d{3})(\\\\d{4})","$1 $2",null,null]]]',
"967": '["YE","00","0",null,null,"$NP$FG","\\\\d{6,9}","[1-7]\\\\d{6,8}",[["([1-7])(\\\\d{3})(\\\\d{3,4})","$1 $2 $3","[1-6]|7[24-68]",null],["(7\\\\d{2})(\\\\d{3})(\\\\d{3})","$1 $2 $3","7[0137]",null]]]',
"49": '["DE","00","0",null,null,"$NP$FG","\\\\d{2,15}","[1-35-9]\\\\d{3,14}|4(?:[0-8]\\\\d{3,12}|9(?:[0-37]\\\\d|4(?:[1-35-8]|4\\\\d?)|5\\\\d{1,2}|6[1-8]\\\\d?)\\\\d{2,8})",[["(1\\\\d{2})(\\\\d{7,8})","$1 $2","1[67]",null],["(15\\\\d{3})(\\\\d{6})","$1 $2","15[0568]",null],["(1\\\\d{3})(\\\\d{7})","$1 $2","15",null],["(\\\\d{2})(\\\\d{3,11})","$1 $2","3[02]|40|[68]9",null],["(\\\\d{3})(\\\\d{3,11})","$1 $2","2(?:\\\\d1|0[2389]|1[24]|28|34)|3(?:[3-9][15]|40)|[4-8][1-9]1|9(?:06|[1-9]1)",null],["(\\\\d{4})(\\\\d{2,11})","$1 $2","[24-6]|[7-9](?:\\\\d[1-9]|[1-9]\\\\d)|3(?:[3569][02-46-9]|4[2-4679]|7[2-467]|8[2-46-8])",null],["(3\\\\d{4})(\\\\d{1,10})","$1 $2","3",null],["(800)(\\\\d{7,12})","$1 $2","800",null],["(\\\\d{3})(\\\\d)(\\\\d{4,10})","$1 $2 $3","(?:18|90)0|137",null],["(1\\\\d{2})(\\\\d{5,11})","$1 $2","181",null],["(18\\\\d{3})(\\\\d{6})","$1 $2","185",null],["(18\\\\d{2})(\\\\d{7})","$1 $2","18[68]",null],["(18\\\\d)(\\\\d{8})","$1 $2","18[2-579]",null],["(700)(\\\\d{4})(\\\\d{4})","$1 $2 $3","700",null],["(138)(\\\\d{4})","$1 $2","138",null],["(15[013-68])(\\\\d{2})(\\\\d{8})","$1 $2 $3","15[013-68]",null],["(15[279]\\\\d)(\\\\d{2})(\\\\d{7})","$1 $2 $3","15[279]",null],["(1[67]\\\\d)(\\\\d{2})(\\\\d{7,8})","$1 $2 $3","1(?:6[023]|7)",null]]]',
"31": '["NL","00","0",null,null,"$NP$FG","\\\\d{5,10}","1\\\\d{4,8}|[2-7]\\\\d{8}|[89]\\\\d{6,9}",[["([1-578]\\\\d)(\\\\d{3})(\\\\d{4})","$1 $2 $3","1[035]|2[0346]|3[03568]|4[0356]|5[0358]|7|8[4578]",null],["([1-5]\\\\d{2})(\\\\d{3})(\\\\d{3})","$1 $2 $3","1[16-8]|2[259]|3[124]|4[17-9]|5[124679]",null],["(6)(\\\\d{8})","$1 $2","6[0-57-9]",null],["(66)(\\\\d{7})","$1 $2","66",null],["(14)(\\\\d{3,4})","$1 $2","14","$FG"],["([89]0\\\\d)(\\\\d{4,7})","$1 $2","80|9",null]]]',
"970": '["PS","00","0",null,null,"$NP$FG","\\\\d{4,10}","[24589]\\\\d{7,8}|1(?:[78]\\\\d{8}|[49]\\\\d{2,3})",[["([2489])(2\\\\d{2})(\\\\d{4})","$1 $2 $3","[2489]",null],["(5[69]\\\\d)(\\\\d{3})(\\\\d{3})","$1 $2 $3","5",null],["(1[78]00)(\\\\d{3})(\\\\d{3})","$1 $2 $3","1[78]","$FG"]]]',
"58": '["VE","00","0",null,null,"$NP$FG","\\\\d{7,10}","[24589]\\\\d{9}",[["(\\\\d{3})(\\\\d{7})","$1-$2",null,null]]]',
"856": '["LA","00","0",null,null,"$NP$FG","\\\\d{6,10}","[2-8]\\\\d{7,9}",[["(20)(\\\\d{2})(\\\\d{3})(\\\\d{3})","$1 $2 $3 $4","20",null],["([2-8]\\\\d)(\\\\d{3})(\\\\d{3})","$1 $2 $3","2[13]|3[14]|[4-8]",null],["(30)(\\\\d{2})(\\\\d{2})(\\\\d{3})","$1 $2 $3 $4","30",null]]]',
"354": '["IS","1(?:0(?:01|10|20)|100)|00",null,null,null,null,"\\\\d{7,9}","[4-9]\\\\d{6}|38\\\\d{7}",[["(\\\\d{3})(\\\\d{4})","$1 $2","[4-9]",null],["(3\\\\d{2})(\\\\d{3})(\\\\d{3})","$1 $2 $3","3",null]]]',
"242": '["CG","00",null,null,null,null,"\\\\d{9}","[028]\\\\d{8}",[["(\\\\d{2})(\\\\d{3})(\\\\d{4})","$1 $2 $3","[02]",null],["(\\\\d)(\\\\d{4})(\\\\d{4})","$1 $2 $3","8",null]]]',
"423": '["LI","00","0","0|10(?:01|20|66)",null,null,"\\\\d{7,9}","6\\\\d{8}|[23789]\\\\d{6}",[["(\\\\d{3})(\\\\d{2})(\\\\d{2})","$1 $2 $3","[23789]",null],["(\\\\d{3})(\\\\d{3})(\\\\d{3})","$1 $2 $3","6[56]",null],["(69)(7\\\\d{2})(\\\\d{4})","$1 $2 $3","697",null]]]',
"213": '["DZ","00","0",null,null,"$NP$FG","\\\\d{8,9}","(?:[1-4]|[5-9]\\\\d)\\\\d{7}",[["([1-4]\\\\d)(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4","[1-4]",null],["([5-8]\\\\d{2})(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4","[5-8]",null],["(9\\\\d)(\\\\d{3})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4","9",null]]]',
"371": '["LV","00",null,null,null,null,"\\\\d{8}","[2689]\\\\d{7}",[["([2689]\\\\d)(\\\\d{3})(\\\\d{3})","$1 $2 $3",null,null]]]',
"503": '["SV","00",null,null,null,null,"\\\\d{7,8}|\\\\d{11}","[267]\\\\d{7}|[89]\\\\d{6}(?:\\\\d{4})?",[["(\\\\d{4})(\\\\d{4})","$1 $2","[267]",null],["(\\\\d{3})(\\\\d{4})","$1 $2","[89]",null],["(\\\\d{3})(\\\\d{4})(\\\\d{4})","$1 $2 $3","[89]",null]]]',
"685": '["WS","0",null,null,null,null,"\\\\d{5,7}","[2-8]\\\\d{4,6}",[["(8\\\\d{2})(\\\\d{3,4})","$1 $2","8",null],["(7\\\\d)(\\\\d{5})","$1 $2","7",null],["(\\\\d{5})","$1","[2-6]",null]]]',
"880": '["BD","00","0",null,null,"$NP$FG","\\\\d{6,10}","[2-79]\\\\d{5,9}|1\\\\d{9}|8[0-7]\\\\d{4,8}",[["(2)(\\\\d{7,8})","$1-$2","2",null],["(\\\\d{2})(\\\\d{4,6})","$1-$2","[3-79]1",null],["(\\\\d{4})(\\\\d{3,6})","$1-$2","1|3(?:0|[2-58]2)|4(?:0|[25]2|3[23]|[4689][25])|5(?:[02-578]2|6[25])|6(?:[0347-9]2|[26][25])|7[02-9]2|8(?:[023][23]|[4-7]2)|9(?:[02][23]|[458]2|6[016])",null],["(\\\\d{3})(\\\\d{3,7})","$1-$2","[3-79][2-9]|8",null]]]',
"265": '["MW","00","0",null,null,"$NP$FG","\\\\d{7,9}","(?:1(?:\\\\d{2})?|[2789]\\\\d{2})\\\\d{6}",[["(\\\\d)(\\\\d{3})(\\\\d{3})","$1 $2 $3","1",null],["(2\\\\d{2})(\\\\d{3})(\\\\d{3})","$1 $2 $3","2",null],["(\\\\d{3})(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4","[1789]",null]]]',
"65": '["SG","0[0-3]\\\\d",null,null,null,null,"\\\\d{8,11}","[36]\\\\d{7}|[17-9]\\\\d{7,10}",[["([3689]\\\\d{3})(\\\\d{4})","$1 $2","[369]|8[1-9]",null],["(1[89]00)(\\\\d{3})(\\\\d{4})","$1 $2 $3","1[89]",null],["(7000)(\\\\d{4})(\\\\d{3})","$1 $2 $3","70",null],["(800)(\\\\d{3})(\\\\d{4})","$1 $2 $3","80",null]]]',
"504": '["HN","00",null,null,null,null,"\\\\d{8}","[237-9]\\\\d{7}",[["(\\\\d{4})(\\\\d{4})","$1-$2",null,null]]]',
"688": '["TV","00",null,null,null,null,"\\\\d{5,7}","[279]\\\\d{4,6}"]',
"84": '["VN","00","0",null,null,"$NP$FG","\\\\d{7,10}","[167]\\\\d{6,9}|[2-59]\\\\d{7,9}|8\\\\d{6,8}",[["([17]99)(\\\\d{4})","$1 $2","[17]99",null],["([48])(\\\\d{4})(\\\\d{4})","$1 $2 $3","4|8(?:[1-57]|6[0-79]|9[0-7])",null],["([235-7]\\\\d)(\\\\d{4})(\\\\d{3})","$1 $2 $3","2[025-79]|3[0136-9]|5[2-9]|6[0-46-8]|7[02-79]",null],["(80)(\\\\d{5})","$1 $2","80",null],["(69\\\\d)(\\\\d{4,5})","$1 $2","69",null],["([235-7]\\\\d{2})(\\\\d{4})(\\\\d{3})","$1 $2 $3","2[0-489]|3[25]|5[01]|65|7[18]",null],["([89]\\\\d)(\\\\d{3})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4","8(?:68|8|9[89])|9",null],["(1[2689]\\\\d)(\\\\d{3})(\\\\d{4})","$1 $2 $3","1(?:[26]|8[68]|99)",null],["(1[89]00)(\\\\d{4,6})","$1 $2","1[89]0","$FG"]]]',
"255": '["TZ","00[056]","0",null,null,"$NP$FG","\\\\d{7,9}","\\\\d{9}",[["([24]\\\\d)(\\\\d{3})(\\\\d{4})","$1 $2 $3","[24]",null],["([67]\\\\d{2})(\\\\d{3})(\\\\d{3})","$1 $2 $3","[67]",null],["([89]\\\\d{2})(\\\\d{2})(\\\\d{4})","$1 $2 $3","[89]",null]]]',
"222": '["MR","00",null,null,null,null,"\\\\d{8}","[2-48]\\\\d{7}",[["([2-48]\\\\d)(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4",null,null]]]',
"230": '["MU","0(?:0|[2-7]0|33)",null,null,null,null,"\\\\d{7,8}","[2-9]\\\\d{6,7}",[["([2-46-9]\\\\d{2})(\\\\d{4})","$1 $2","[2-46-9]",null],["(5\\\\d{3})(\\\\d{4})","$1 $2","5",null]]]',
"592": '["GY","001",null,null,null,null,"\\\\d{7}","[2-46-9]\\\\d{6}",[["(\\\\d{3})(\\\\d{4})","$1 $2",null,null]]]',
"41": '["CH","00","0",null,null,"$NP$FG","\\\\d{9}(?:\\\\d{3})?","[2-9]\\\\d{8}|860\\\\d{9}",[["([2-9]\\\\d)(\\\\d{3})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4","[2-7]|[89]1",null],["([89]\\\\d{2})(\\\\d{3})(\\\\d{3})","$1 $2 $3","8[047]|90",null],["(\\\\d{3})(\\\\d{2})(\\\\d{3})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4 $5","860",null]]]',
"39": ['["IT","00",null,null,null,null,"\\\\d{6,11}","[01589]\\\\d{5,10}|3(?:[12457-9]\\\\d{8}|[36]\\\\d{7,9})",[["(\\\\d{2})(\\\\d{3,4})(\\\\d{4})","$1 $2 $3","0[26]|55",null],["(0[26])(\\\\d{4})(\\\\d{5})","$1 $2 $3","0[26]",null],["(0[26])(\\\\d{4,6})","$1 $2","0[26]",null],["(0\\\\d{2})(\\\\d{3,4})(\\\\d{4})","$1 $2 $3","0[13-57-9][0159]",null],["(\\\\d{3})(\\\\d{3,6})","$1 $2","0[13-57-9][0159]|8(?:03|4[17]|9[245])",null],["(0\\\\d{3})(\\\\d{3})(\\\\d{4})","$1 $2 $3","0[13-57-9][2-46-8]",null],["(0\\\\d{3})(\\\\d{2,6})","$1 $2","0[13-57-9][2-46-8]",null],["(\\\\d{3})(\\\\d{3})(\\\\d{3,4})","$1 $2 $3","[13]|8(?:00|4[08]|9[59])",null],["(\\\\d{4})(\\\\d{4})","$1 $2","894",null],["(\\\\d{3})(\\\\d{4})(\\\\d{4})","$1 $2 $3","3",null]]]','["VA","00",null,null,null,null,"\\\\d{6,11}","(?:0(?:878\\\\d{5}|6698\\\\d{5})|[1589]\\\\d{5,10}|3(?:[12457-9]\\\\d{8}|[36]\\\\d{7,9}))"]'],
"993": '["TM","810","8",null,null,"($NP $FG)","\\\\d{8}","[1-6]\\\\d{7}",[["(\\\\d{2})(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2-$3-$4","12",null],["(\\\\d{2})(\\\\d{6})","$1 $2","6","$NP $FG"],["(\\\\d{3})(\\\\d)(\\\\d{2})(\\\\d{2})","$1 $2-$3-$4","13|[2-5]",null]]]',
"888": '["001",null,null,null,null,null,"\\\\d{11}","\\\\d{11}",[["(\\\\d{3})(\\\\d{3})(\\\\d{5})","$1 $2 $3",null,null]]]',
"353": '["IE","00","0",null,null,"($NP$FG)","\\\\d{5,10}","[124-9]\\\\d{6,9}",[["(1)(\\\\d{3,4})(\\\\d{4})","$1 $2 $3","1",null],["(\\\\d{2})(\\\\d{5})","$1 $2","2[24-9]|47|58|6[237-9]|9[35-9]",null],["(\\\\d{3})(\\\\d{5})","$1 $2","40[24]|50[45]",null],["(48)(\\\\d{4})(\\\\d{4})","$1 $2 $3","48",null],["(818)(\\\\d{3})(\\\\d{3})","$1 $2 $3","81",null],["(\\\\d{2})(\\\\d{3})(\\\\d{3,4})","$1 $2 $3","[24-69]|7[14]",null],["([78]\\\\d)(\\\\d{3,4})(\\\\d{4})","$1 $2 $3","76|8[35-9]","$NP$FG"],["(700)(\\\\d{3})(\\\\d{3})","$1 $2 $3","70","$NP$FG"],["(\\\\d{4})(\\\\d{3})(\\\\d{3})","$1 $2 $3","1(?:8[059]|5)","$FG"]]]',
"966": '["SA","00","0",null,null,"$NP$FG","\\\\d{7,10}","1\\\\d{7,8}|(?:[2-467]|92)\\\\d{7}|5\\\\d{8}|8\\\\d{9}",[["([1-467])(\\\\d{3})(\\\\d{4})","$1 $2 $3","[1-467]",null],["(1\\\\d)(\\\\d{3})(\\\\d{4})","$1 $2 $3","1[1-467]",null],["(5\\\\d)(\\\\d{3})(\\\\d{4})","$1 $2 $3","5",null],["(92\\\\d{2})(\\\\d{5})","$1 $2","92","$FG"],["(800)(\\\\d{3})(\\\\d{4})","$1 $2 $3","80","$FG"],["(811)(\\\\d{3})(\\\\d{3,4})","$1 $2 $3","81",null]]]',
"380": '["UA","00","0",null,null,"$NP$FG","\\\\d{5,9}","[3-9]\\\\d{8}",[["([3-9]\\\\d)(\\\\d{3})(\\\\d{4})","$1 $2 $3","[38]9|4(?:[45][0-5]|87)|5(?:0|6[37]|7[37])|6[36-8]|7|9[1-9]",null],["([3-689]\\\\d{2})(\\\\d{3})(\\\\d{3})","$1 $2 $3","3[1-8]2|4[13678]2|5(?:[12457]2|6[24])|6(?:[49]2|[12][29]|5[24])|8[0-8]|90",null],["([3-6]\\\\d{3})(\\\\d{5})","$1 $2","3(?:5[013-9]|[1-46-8])|4(?:[137][013-9]|6|[45][6-9]|8[4-6])|5(?:[1245][013-9]|6[0135-9]|3|7[4-6])|6(?:[49][013-9]|5[0135-9]|[12][13-8])",null]]]',
"98": '["IR","00","0",null,null,"$NP$FG","\\\\d{4,10}","[1-8]\\\\d{9}|9(?:[0-4]\\\\d{8}|9\\\\d{2,8})",[["(21)(\\\\d{3,5})","$1 $2","21",null],["(\\\\d{2})(\\\\d{4})(\\\\d{4})","$1 $2 $3","[1-8]",null],["(\\\\d{3})(\\\\d{3})","$1 $2","9",null],["(\\\\d{3})(\\\\d{2})(\\\\d{2,3})","$1 $2 $3","9",null],["(\\\\d{3})(\\\\d{3})(\\\\d{3,4})","$1 $2 $3","9",null]]]',
"971": '["AE","00","0",null,null,"$NP$FG","\\\\d{5,12}","[2-79]\\\\d{7,8}|800\\\\d{2,9}",[["([2-4679])(\\\\d{3})(\\\\d{4})","$1 $2 $3","[2-4679][2-8]",null],["(5\\\\d)(\\\\d{3})(\\\\d{4})","$1 $2 $3","5",null],["([479]00)(\\\\d)(\\\\d{5})","$1 $2 $3","[479]0","$FG"],["([68]00)(\\\\d{2,9})","$1 $2","60|8","$FG"]]]',
"30": '["GR","00",null,null,null,null,"\\\\d{10}","[26-9]\\\\d{9}",[["([27]\\\\d)(\\\\d{4})(\\\\d{4})","$1 $2 $3","21|7",null],["(\\\\d{3})(\\\\d{3})(\\\\d{4})","$1 $2 $3","2[2-9]1|[689]",null],["(2\\\\d{3})(\\\\d{6})","$1 $2","2[2-9][02-9]",null]]]',
"228": '["TG","00",null,null,null,null,"\\\\d{8}","[29]\\\\d{7}",[["(\\\\d{2})(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4","[29]",null]]]',
"48": '["PL","00",null,null,null,null,"\\\\d{6,9}","[12]\\\\d{6,8}|[3-57-9]\\\\d{8}|6\\\\d{5,8}",[["(\\\\d{2})(\\\\d{3})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4","[14]|2[0-57-9]|3[2-4]|5[24-689]|6[1-3578]|7[14-7]|8[1-79]|9[145]",null],["(\\\\d{2})(\\\\d{1})(\\\\d{4})","$1 $2 $3","[12]2",null],["(\\\\d{3})(\\\\d{3})(\\\\d{3})","$1 $2 $3","26|39|5[0137]|6[0469]|7[02389]|8[08]",null],["(\\\\d{3})(\\\\d{2})(\\\\d{2,3})","$1 $2 $3","64",null],["(\\\\d{3})(\\\\d{3})","$1 $2","64",null]]]',
"886": '["TW","0(?:0[25679]|19)","0",null,null,"$NP$FG","\\\\d{7,10}","2\\\\d{6,8}|[3-689]\\\\d{7,8}|7\\\\d{7,9}",[["(20)(\\\\d)(\\\\d{4})","$1 $2 $3","202",null],["(20)(\\\\d{3})(\\\\d{4})","$1 $2 $3","20[013-9]",null],["([2-8])(\\\\d{3,4})(\\\\d{4})","$1 $2 $3","2[23-8]|[3-6]|[78][1-9]",null],["([89]\\\\d{2})(\\\\d{3})(\\\\d{3})","$1 $2 $3","80|9",null],["(70)(\\\\d{4})(\\\\d{4})","$1 $2 $3","70",null]]]',
"212": ['["MA","00","0",null,null,"$NP$FG","\\\\d{9}","[5-9]\\\\d{8}",[["([5-7]\\\\d{2})(\\\\d{6})","$1-$2","5(?:2[015-7]|3[0-4])|[67]",null],["([58]\\\\d{3})(\\\\d{5})","$1-$2","5(?:2[2-489]|3[5-9]|92)|892",null],["(5\\\\d{4})(\\\\d{4})","$1-$2","5(?:29|38)",null],["([5]\\\\d{2})(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4","5(?:4[067]|5[03])",null],["(8[09])(\\\\d{7})","$1-$2","8(?:0|9[013-9])",null]]]','["EH","00","0",null,null,"$NP$FG","\\\\d{9}","[5-9]\\\\d{8}"]'],
"372": '["EE","00",null,null,null,null,"\\\\d{4,10}","1\\\\d{3,4}|[3-9]\\\\d{6,7}|800\\\\d{6,7}",[["([3-79]\\\\d{2})(\\\\d{4})","$1 $2","[369]|4[3-8]|5(?:[0-2]|5[0-478]|6[45])|7[1-9]",null],["(70)(\\\\d{2})(\\\\d{4})","$1 $2 $3","70",null],["(8000)(\\\\d{3})(\\\\d{3})","$1 $2 $3","800",null],["([458]\\\\d{3})(\\\\d{3,4})","$1 $2","40|5|8(?:00|[1-5])",null]]]',
"598": '["UY","0(?:1[3-9]\\\\d|0)","0",null,null,null,"\\\\d{7,8}","[2489]\\\\d{6,7}",[["(\\\\d{4})(\\\\d{4})","$1 $2","[24]",null],["(\\\\d{2})(\\\\d{3})(\\\\d{3})","$1 $2 $3","9[1-9]","$NP$FG"],["(\\\\d{3})(\\\\d{4})","$1 $2","[89]0","$NP$FG"]]]',
"502": '["GT","00",null,null,null,null,"\\\\d{8}(?:\\\\d{3})?","[2-7]\\\\d{7}|1[89]\\\\d{9}",[["(\\\\d{4})(\\\\d{4})","$1 $2","[2-7]",null],["(\\\\d{4})(\\\\d{3})(\\\\d{4})","$1 $2 $3","1",null]]]',
"82": '["KR","00(?:[124-68]|3\\\\d{2}|7(?:[0-8]\\\\d|9[0-79]))","0","0(8[1-46-8]|85\\\\d{2})?",null,"$NP$FG","\\\\d{3,14}","007\\\\d{9,11}|[1-7]\\\\d{3,9}|8\\\\d{8}",[["(\\\\d{5})(\\\\d{3,4})(\\\\d{4})","$1 $2 $3","00798","$FG","NA"],["(\\\\d{5})(\\\\d{2})(\\\\d{3})(\\\\d{4})","$1 $2 $3 $4","00798","$FG","NA"],["(\\\\d{2})(\\\\d{4})(\\\\d{4})","$1-$2-$3","1(?:0|1[19]|[69]9|5[458])|[57]0",null],["(\\\\d{2})(\\\\d{3,4})(\\\\d{4})","$1-$2-$3","1(?:[01]|5[1-4]|6[2-8]|[7-9])|[68]0|[3-6][1-9][1-9]",null],["(\\\\d{3})(\\\\d)(\\\\d{4})","$1-$2-$3","131",null],["(\\\\d{3})(\\\\d{2})(\\\\d{4})","$1-$2-$3","131",null],["(\\\\d{3})(\\\\d{3})(\\\\d{4})","$1-$2-$3","13[2-9]",null],["(\\\\d{2})(\\\\d{2})(\\\\d{3})(\\\\d{4})","$1-$2-$3-$4","30",null],["(\\\\d)(\\\\d{3,4})(\\\\d{4})","$1-$2-$3","2[1-9]",null],["(\\\\d)(\\\\d{3,4})","$1-$2","21[0-46-9]",null],["(\\\\d{2})(\\\\d{3,4})","$1-$2","[3-6][1-9]1",null],["(\\\\d{4})(\\\\d{4})","$1-$2","1(?:5[246-9]|6[04678]|8[03579])","$FG"]]]',
"253": '["DJ","00",null,null,null,null,"\\\\d{8}","[27]\\\\d{7}",[["(\\\\d{2})(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4",null,null]]]',
"91": '["IN","00","0",null,null,"$NP$FG","\\\\d{6,13}","008\\\\d{9}|1\\\\d{7,12}|[2-9]\\\\d{9,10}",[["(\\\\d{5})(\\\\d{5})","$1 $2","600|7(?:[02-8]|19|9[037-9])|8(?:0[015-9]|[1-9]|20)|9",null],["(\\\\d{2})(\\\\d{4})(\\\\d{4})","$1 $2 $3","11|2[02]|33|4[04]|79[1-9]|80[2-46]",null],["(\\\\d{3})(\\\\d{3})(\\\\d{4})","$1 $2 $3","1(?:2[0-249]|3[0-25]|4[145]|[59][14]|7[1257]|[68][1-9])|2(?:1[257]|3[013]|4[01]|5[0137]|6[0158]|78|8[1568]|9[14])|3(?:26|4[1-3]|5[34]|6[01489]|7[02-46]|8[159])|4(?:1[36]|2[1-47]|3[15]|5[12]|6[0-26-9]|7[0-24-9]|8[013-57]|9[014-7])|5(?:1[025]|[36][25]|22|4[28]|5[12]|[78]1|9[15])|6(?:12|[2-4]1|5[17]|6[13]|7[14]|80)|7(?:12|2[14]|3[134]|4[47]|5[15]|[67]1|88)|8(?:16|2[014]|3[126]|6[136]|7[078]|8[34]|91)",null],["(\\\\d{4})(\\\\d{3})(\\\\d{3})","$1 $2 $3","1(?:[23579]|[468][1-9])|[2-8]",null],["(\\\\d{2})(\\\\d{3})(\\\\d{4})(\\\\d{3})","$1 $2 $3 $4","008",null],["(\\\\d{3})(\\\\d{3})(\\\\d{4})","$1 $2 $3","140","$FG"],["(\\\\d{4})(\\\\d{2})(\\\\d{4})","$1 $2 $3","160","$FG"],["(\\\\d{4})(\\\\d{4,5})","$1 $2","180","$FG"],["(\\\\d{4})(\\\\d{2,4})(\\\\d{4})","$1 $2 $3","180","$FG"],["(\\\\d{4})(\\\\d{3,4})(\\\\d{4})","$1 $2 $3","186","$FG"],["(\\\\d{4})(\\\\d{3})(\\\\d{3})(\\\\d{3})","$1 $2 $3 $4","18[06]","$FG"]]]',
"389": '["MK","00","0",null,null,"$NP$FG","\\\\d{6,8}","[2-578]\\\\d{7}",[["(2)(\\\\d{3})(\\\\d{4})","$1 $2 $3","2",null],["([347]\\\\d)(\\\\d{3})(\\\\d{3})","$1 $2 $3","[347]",null],["([58]\\\\d{2})(\\\\d)(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4","[58]",null]]]',
"1": ['["US","011","1",null,null,null,"\\\\d{7}(?:\\\\d{3})?","[2-9]\\\\d{9}",[["(\\\\d{3})(\\\\d{4})","$1-$2",null,null,"NA"],["(\\\\d{3})(\\\\d{3})(\\\\d{4})","($1) $2-$3",null,null,"$1-$2-$3"]]]','["AI","011","1",null,null,null,"\\\\d{7}(?:\\\\d{3})?","[2589]\\\\d{9}"]','["AS","011","1",null,null,null,"\\\\d{7}(?:\\\\d{3})?","[5689]\\\\d{9}"]','["BB","011","1",null,null,null,"\\\\d{7}(?:\\\\d{3})?","[2589]\\\\d{9}"]','["BM","011","1",null,null,null,"\\\\d{7}(?:\\\\d{3})?","[4589]\\\\d{9}"]','["BS","011","1",null,null,null,"\\\\d{7}(?:\\\\d{3})?","[2589]\\\\d{9}"]','["CA","011","1",null,null,null,"\\\\d{7}(?:\\\\d{3})?","[2-9]\\\\d{9}|3\\\\d{6}"]','["DM","011","1",null,null,null,"\\\\d{7}(?:\\\\d{3})?","[57-9]\\\\d{9}"]','["DO","011","1",null,null,null,"\\\\d{7}(?:\\\\d{3})?","[589]\\\\d{9}"]','["GD","011","1",null,null,null,"\\\\d{7}(?:\\\\d{3})?","[4589]\\\\d{9}"]','["GU","011","1",null,null,null,"\\\\d{7}(?:\\\\d{3})?","[5689]\\\\d{9}"]','["JM","011","1",null,null,null,"\\\\d{7}(?:\\\\d{3})?","[589]\\\\d{9}"]','["KN","011","1",null,null,null,"\\\\d{7}(?:\\\\d{3})?","[589]\\\\d{9}"]','["KY","011","1",null,null,null,"\\\\d{7}(?:\\\\d{3})?","[3589]\\\\d{9}"]','["LC","011","1",null,null,null,"\\\\d{7}(?:\\\\d{3})?","[5789]\\\\d{9}"]','["MP","011","1",null,null,null,"\\\\d{7}(?:\\\\d{3})?","[5689]\\\\d{9}"]','["MS","011","1",null,null,null,"\\\\d{7}(?:\\\\d{3})?","[5689]\\\\d{9}"]','["PR","011","1",null,null,null,"\\\\d{7}(?:\\\\d{3})?","[5789]\\\\d{9}"]','["SX","011","1",null,null,null,"\\\\d{7}(?:\\\\d{3})?","[5789]\\\\d{9}"]','["TC","011","1",null,null,null,"\\\\d{7}(?:\\\\d{3})?","[5689]\\\\d{9}"]','["TT","011","1",null,null,null,"\\\\d{7}(?:\\\\d{3})?","[589]\\\\d{9}"]','["AG","011","1",null,null,null,"\\\\d{7}(?:\\\\d{3})?","[2589]\\\\d{9}"]','["VC","011","1",null,null,null,"\\\\d{7}(?:\\\\d{3})?","[5789]\\\\d{9}"]','["VG","011","1",null,null,null,"\\\\d{7}(?:\\\\d{3})?","[2589]\\\\d{9}"]','["VI","011","1",null,null,null,"\\\\d{7}(?:\\\\d{3})?","[3589]\\\\d{9}"]'],
"60": '["MY","00","0",null,null,null,"\\\\d{6,10}","[13-9]\\\\d{7,9}",[["([4-79])(\\\\d{3})(\\\\d{4})","$1-$2 $3","[4-79]","$NP$FG"],["(3)(\\\\d{4})(\\\\d{4})","$1-$2 $3","3","$NP$FG"],["([18]\\\\d)(\\\\d{3})(\\\\d{3,4})","$1-$2 $3","1[02-46-9][1-9]|8","$NP$FG"],["(1)([36-8]00)(\\\\d{2})(\\\\d{4})","$1-$2-$3-$4","1[36-8]0",null],["(11)(\\\\d{4})(\\\\d{4})","$1-$2 $3","11","$NP$FG"],["(15[49])(\\\\d{3})(\\\\d{4})","$1-$2 $3","15","$NP$FG"]]]',
"355": '["AL","00","0",null,null,"$NP$FG","\\\\d{5,9}","[2-57]\\\\d{7}|6\\\\d{8}|8\\\\d{5,7}|9\\\\d{5}",[["(4)(\\\\d{3})(\\\\d{4})","$1 $2 $3","4[0-6]",null],["(6\\\\d)(\\\\d{3})(\\\\d{4})","$1 $2 $3","6",null],["(\\\\d{2})(\\\\d{3})(\\\\d{3})","$1 $2 $3","[2358][2-5]|4[7-9]",null],["(\\\\d{3})(\\\\d{3,5})","$1 $2","[235][16-9]|8[016-9]|[79]",null]]]',
"254": '["KE","000","0","005|0",null,"$NP$FG","\\\\d{7,10}","20\\\\d{6,7}|[4-9]\\\\d{6,9}",[["(\\\\d{2})(\\\\d{5,7})","$1 $2","[24-6]",null],["(\\\\d{3})(\\\\d{6})","$1 $2","7",null],["(\\\\d{3})(\\\\d{3})(\\\\d{3,4})","$1 $2 $3","[89]",null]]]',
"223": '["ML","00",null,null,null,null,"\\\\d{8}","[246-9]\\\\d{7}",[["(\\\\d{2})(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4","[246-9]",null],["(\\\\d{4})","$1","67|74",null,"NA"]]]',
"686": '["KI","00",null,"0",null,null,"\\\\d{5,8}","[2458]\\\\d{4}|3\\\\d{4,7}|7\\\\d{7}"]',
"994": '["AZ","00","0",null,null,"($NP$FG)","\\\\d{7,9}","[1-9]\\\\d{8}",[["(\\\\d{2})(\\\\d{3})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4","(?:1[28]|2(?:[45]2|[0-36])|365)",null],["(\\\\d{2})(\\\\d{3})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4","[4-8]","$NP$FG"],["(\\\\d{3})(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4","9","$NP$FG"]]]',
"979": '["001",null,null,null,null,null,"\\\\d{9}","\\\\d{9}",[["(\\\\d)(\\\\d{4})(\\\\d{4})","$1 $2 $3",null,null]]]',
"66": '["TH","00","0",null,null,"$NP$FG","\\\\d{4}|\\\\d{8,10}","[2-9]\\\\d{7,8}|1\\\\d{3}(?:\\\\d{5,6})?",[["(2)(\\\\d{3})(\\\\d{4})","$1 $2 $3","2",null],["([13-9]\\\\d)(\\\\d{3})(\\\\d{3,4})","$1 $2 $3","14|[3-9]",null],["(1[89]00)(\\\\d{3})(\\\\d{3})","$1 $2 $3","1","$FG"]]]',
"233": '["GH","00","0",null,null,"$NP$FG","\\\\d{7,9}","[235]\\\\d{8}|8\\\\d{7}",[["(\\\\d{2})(\\\\d{3})(\\\\d{4})","$1 $2 $3","[235]",null],["(\\\\d{3})(\\\\d{5})","$1 $2","8",null]]]',
"593": '["EC","00","0",null,null,"($NP$FG)","\\\\d{7,11}","1\\\\d{9,10}|[2-8]\\\\d{7}|9\\\\d{8}",[["(\\\\d)(\\\\d{3})(\\\\d{4})","$1 $2-$3","[247]|[356][2-8]",null,"$1-$2-$3"],["(\\\\d{2})(\\\\d{3})(\\\\d{4})","$1 $2 $3","9","$NP$FG"],["(1800)(\\\\d{3})(\\\\d{3,4})","$1 $2 $3","1","$FG"]]]',
"509": '["HT","00",null,null,null,null,"\\\\d{8}","[2-489]\\\\d{7}",[["(\\\\d{2})(\\\\d{2})(\\\\d{4})","$1 $2 $3",null,null]]]',
"54": '["AR","00","0","0?(?:(11|2(?:2(?:02?|[13]|2[13-79]|4[1-6]|5[2457]|6[124-8]|7[1-4]|8[13-6]|9[1267])|3(?:02?|1[467]|2[03-6]|3[13-8]|[49][2-6]|5[2-8]|[67])|4(?:7[3-578]|9)|6(?:[0136]|2[24-6]|4[6-8]?|5[15-8])|80|9(?:0[1-3]|[19]|2\\\\d|3[1-6]|4[02568]?|5[2-4]|6[2-46]|72?|8[23]?))|3(?:3(?:2[79]|6|8[2578])|4(?:0[0-24-9]|[12]|3[5-8]?|4[24-7]|5[4-68]?|6[02-9]|7[126]|8[2379]?|9[1-36-8])|5(?:1|2[1245]|3[237]?|4[1-46-9]|6[2-4]|7[1-6]|8[2-5]?)|6[24]|7(?:[069]|1[1568]|2[15]|3[145]|4[13]|5[14-8]|7[2-57]|8[126])|8(?:[01]|2[15-7]|3[2578]?|4[13-6]|5[4-8]?|6[1-357-9]|7[36-8]?|8[5-8]?|9[124])))?15)?","9$1","$NP$FG","\\\\d{6,11}","11\\\\d{8}|[2368]\\\\d{9}|9\\\\d{10}",[["([68]\\\\d{2})(\\\\d{3})(\\\\d{4})","$1-$2-$3","[68]",null],["(\\\\d{2})(\\\\d{4})","$1-$2","[2-9]","$FG","NA"],["(\\\\d{3})(\\\\d{4})","$1-$2","[2-9]","$FG","NA"],["(\\\\d{4})(\\\\d{4})","$1-$2","[2-9]","$FG","NA"],["(9)(11)(\\\\d{4})(\\\\d{4})","$2 15-$3-$4","911",null,"$1 $2 $3-$4"],["(9)(\\\\d{3})(\\\\d{3})(\\\\d{4})","$2 15-$3-$4","9(?:2[234689]|3[3-8])",null,"$1 $2 $3-$4"],["(9)(\\\\d{4})(\\\\d{2})(\\\\d{4})","$2 15-$3-$4","9[23]",null,"$1 $2 $3-$4"],["(11)(\\\\d{4})(\\\\d{4})","$1 $2-$3","1",null],["(\\\\d{3})(\\\\d{3})(\\\\d{4})","$1 $2-$3","2(?:2[013]|3[067]|49|6[01346]|80|9[147-9])|3(?:36|4[1-358]|5[138]|6[24]|7[069]|8[013578])",null],["(\\\\d{4})(\\\\d{2})(\\\\d{4})","$1 $2-$3","[23]",null],["(\\\\d{3})","$1","1[012]|911","$FG","NA"]]]',
"57": '["CO","00(?:4(?:[14]4|56)|[579])","0","0([3579]|4(?:44|56))?",null,null,"\\\\d{7,11}","(?:[13]\\\\d{0,3}|[24-8])\\\\d{7}",[["(\\\\d)(\\\\d{7})","$1 $2","1(?:8[2-9]|9[0-3]|[2-7])|[24-8]","($FG)"],["(\\\\d{3})(\\\\d{7})","$1 $2","3",null],["(1)(\\\\d{3})(\\\\d{7})","$1-$2-$3","1(?:80|9[04])","$NP$FG","$1 $2 $3"]]]',
"597": '["SR","00",null,null,null,null,"\\\\d{6,7}","[2-8]\\\\d{5,6}",[["(\\\\d{3})(\\\\d{3})","$1-$2","[2-4]|5[2-58]",null],["(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1-$2-$3","56",null],["(\\\\d{3})(\\\\d{4})","$1-$2","[6-8]",null]]]',
"676": '["TO","00",null,null,null,null,"\\\\d{5,7}","[02-8]\\\\d{4,6}",[["(\\\\d{2})(\\\\d{3})","$1-$2","[1-6]|7[0-4]|8[05]",null],["(\\\\d{3})(\\\\d{4})","$1 $2","7[5-9]|8[47-9]",null],["(\\\\d{4})(\\\\d{3})","$1 $2","0",null]]]',
"505": '["NI","00",null,null,null,null,"\\\\d{8}","[12578]\\\\d{7}",[["(\\\\d{4})(\\\\d{4})","$1 $2",null,null]]]',
"850": '["KP","00|99","0",null,null,"$NP$FG","\\\\d{6,8}|\\\\d{10}","1\\\\d{9}|[28]\\\\d{7}",[["(\\\\d{3})(\\\\d{3})(\\\\d{4})","$1 $2 $3","1",null],["(\\\\d)(\\\\d{3})(\\\\d{4})","$1 $2 $3","2",null],["(\\\\d{2})(\\\\d{3})(\\\\d{3})","$1 $2 $3","8",null]]]',
"7": ['["RU","810","8",null,null,"$NP ($FG)","\\\\d{10}","[3489]\\\\d{9}",[["(\\\\d{3})(\\\\d{2})(\\\\d{2})","$1-$2-$3","[1-79]","$FG","NA"],["([3489]\\\\d{2})(\\\\d{3})(\\\\d{2})(\\\\d{2})","$1 $2-$3-$4","[34689]",null],["(7\\\\d{2})(\\\\d{3})(\\\\d{4})","$1 $2 $3","7",null]]]','["KZ","810","8",null,null,null,"\\\\d{10}","(?:33\\\\d|7\\\\d{2}|80[09])\\\\d{7}"]'],
"268": '["SZ","00",null,null,null,null,"\\\\d{8}","[027]\\\\d{7}",[["(\\\\d{4})(\\\\d{4})","$1 $2","[027]",null]]]',
"501": '["BZ","00",null,null,null,null,"\\\\d{7}(?:\\\\d{4})?","[2-8]\\\\d{6}|0\\\\d{10}",[["(\\\\d{3})(\\\\d{4})","$1-$2","[2-8]",null],["(0)(800)(\\\\d{4})(\\\\d{3})","$1-$2-$3-$4","0",null]]]',
"252": '["SO","00","0",null,null,null,"\\\\d{6,9}","[1-9]\\\\d{5,8}",[["(\\\\d{6})","$1","[134]",null],["(\\\\d)(\\\\d{6})","$1 $2","2[0-79]|[13-5]",null],["(\\\\d)(\\\\d{7})","$1 $2","24|[67]",null],["(\\\\d{2})(\\\\d{4})","$1 $2","8[125]",null],["(\\\\d{2})(\\\\d{5,7})","$1 $2","15|28|6[1-35-9]|799|9[2-9]",null],["(\\\\d{3})(\\\\d{3})(\\\\d{3})","$1 $2 $3","3[59]|4[89]|6[24-6]|79|8[08]|90",null]]]',
"229": '["BJ","00",null,null,null,null,"\\\\d{4,8}","[2689]\\\\d{7}|7\\\\d{3}",[["(\\\\d{2})(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4",null,null]]]',
"680": '["PW","01[12]",null,null,null,null,"\\\\d{7}","[2-8]\\\\d{6}",[["(\\\\d{3})(\\\\d{4})","$1 $2",null,null]]]',
"263": '["ZW","00","0",null,null,"$NP$FG","\\\\d{3,10}","2(?:[012457-9]\\\\d{3,8}|6(?:[14]\\\\d{7}|\\\\d{4}))|[13-79]\\\\d{4,9}|8[06]\\\\d{8}",[["([49])(\\\\d{3})(\\\\d{2,4})","$1 $2 $3","4|9[2-9]",null],["(7\\\\d)(\\\\d{3})(\\\\d{3,4})","$1 $2 $3","7",null],["(86\\\\d{2})(\\\\d{3})(\\\\d{3})","$1 $2 $3","86[24]",null],["([2356]\\\\d{2})(\\\\d{3,5})","$1 $2","2(?:0[45]|2[278]|[49]8|[78])|3(?:08|17|3[78]|7[1569]|8[37]|98)|5[15][78]|6(?:[29]8|[38]7|6[78]|75|[89]8)",null],["(\\\\d{3})(\\\\d{3})(\\\\d{3,4})","$1 $2 $3","2(?:1[39]|2[0157]|6[14]|7[35]|84)|329",null],["([1-356]\\\\d)(\\\\d{3,5})","$1 $2","1[3-9]|2[0569]|3[0-69]|5[05689]|6[0-46-9]",null],["([235]\\\\d)(\\\\d{3})(\\\\d{3,4})","$1 $2 $3","[23]9|54",null],["([25]\\\\d{3})(\\\\d{3,5})","$1 $2","(?:25|54)8",null],["(8\\\\d{3})(\\\\d{6})","$1 $2","86",null],["(80\\\\d)(\\\\d{3})(\\\\d{4})","$1 $2 $3","80",null]]]',
"90": '["TR","00","0",null,null,null,"\\\\d{7,10}","[2-589]\\\\d{9}|444\\\\d{4}",[["(\\\\d{3})(\\\\d{3})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4","[23]|4(?:[0-35-9]|4[0-35-9])","($NP$FG)"],["(\\\\d{3})(\\\\d{3})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4","5[02-69]","$NP$FG"],["(\\\\d{3})(\\\\d{3})(\\\\d{4})","$1 $2 $3","51|[89]","$NP$FG"],["(444)(\\\\d{1})(\\\\d{3})","$1 $2 $3","444",null]]]',
"352": '["LU","00",null,"(15(?:0[06]|1[12]|35|4[04]|55|6[26]|77|88|99)\\\\d)",null,null,"\\\\d{4,11}","[24-9]\\\\d{3,10}|3(?:[0-46-9]\\\\d{2,9}|5[013-9]\\\\d{1,8})",[["(\\\\d{2})(\\\\d{3})","$1 $2","[2-5]|7[1-9]|[89](?:[1-9]|0[2-9])",null],["(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2 $3","[2-5]|7[1-9]|[89](?:[1-9]|0[2-9])",null],["(\\\\d{2})(\\\\d{2})(\\\\d{3})","$1 $2 $3","20",null],["(\\\\d{2})(\\\\d{2})(\\\\d{2})(\\\\d{1,2})","$1 $2 $3 $4","2(?:[0367]|4[3-8])",null],["(\\\\d{2})(\\\\d{2})(\\\\d{2})(\\\\d{3})","$1 $2 $3 $4","20",null],["(\\\\d{2})(\\\\d{2})(\\\\d{2})(\\\\d{2})(\\\\d{1,2})","$1 $2 $3 $4 $5","2(?:[0367]|4[3-8])",null],["(\\\\d{2})(\\\\d{2})(\\\\d{2})(\\\\d{1,4})","$1 $2 $3 $4","2(?:[12589]|4[12])|[3-5]|7[1-9]|8(?:[1-9]|0[2-9])|9(?:[1-9]|0[2-46-9])",null],["(\\\\d{3})(\\\\d{2})(\\\\d{3})","$1 $2 $3","70|80[01]|90[015]",null],["(\\\\d{3})(\\\\d{3})(\\\\d{3})","$1 $2 $3","6",null]]]',
"47": ['["NO","00",null,null,null,null,"\\\\d{5}(?:\\\\d{3})?","0\\\\d{4}|[2-9]\\\\d{7}",[["([489]\\\\d{2})(\\\\d{2})(\\\\d{3})","$1 $2 $3","[489]",null],["([235-7]\\\\d)(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4","[235-7]",null]]]','["SJ","00",null,null,null,null,"\\\\d{5}(?:\\\\d{3})?","0\\\\d{4}|[45789]\\\\d{7}"]'],
"243": '["CD","00","0",null,null,"$NP$FG","\\\\d{7,9}","[2-6]\\\\d{6}|[18]\\\\d{6,8}|9\\\\d{8}",[["(\\\\d{2})(\\\\d{3})(\\\\d{4})","$1 $2 $3","12",null],["([89]\\\\d{2})(\\\\d{3})(\\\\d{3})","$1 $2 $3","8[0-2459]|9",null],["(\\\\d{2})(\\\\d{2})(\\\\d{3})","$1 $2 $3","88",null],["(\\\\d{2})(\\\\d{5})","$1 $2","[1-6]",null]]]',
"220": '["GM","00",null,null,null,null,"\\\\d{7}","[2-9]\\\\d{6}",[["(\\\\d{3})(\\\\d{4})","$1 $2",null,null]]]',
"687": '["NC","00",null,null,null,null,"\\\\d{6}","[2-57-9]\\\\d{5}",[["(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1.$2.$3","[2-46-9]|5[0-4]",null]]]',
"995": '["GE","00","0",null,null,null,"\\\\d{6,9}","[34578]\\\\d{8}",[["(\\\\d{3})(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4","[348]","$NP$FG"],["(\\\\d{3})(\\\\d{3})(\\\\d{3})","$1 $2 $3","7","$NP$FG"],["(\\\\d{3})(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4","5","$FG"]]]',
"961": '["LB","00","0",null,null,null,"\\\\d{7,8}","[13-9]\\\\d{6,7}",[["(\\\\d)(\\\\d{3})(\\\\d{3})","$1 $2 $3","[13-6]|7(?:[2-57]|62|8[0-7]|9[04-9])|8[02-9]|9","$NP$FG"],["([7-9]\\\\d)(\\\\d{3})(\\\\d{3})","$1 $2 $3","[89][01]|7(?:[01]|6[013-9]|8[89]|9[1-3])",null]]]',
"40": '["RO","00","0",null,null,"$NP$FG","\\\\d{6,9}","[23]\\\\d{5,8}|[7-9]\\\\d{8}",[["(\\\\d{2})(\\\\d{3})(\\\\d{4})","$1 $2 $3","[23]1",null],["(\\\\d{2})(\\\\d{4})","$1 $2","[23]1",null],["(\\\\d{3})(\\\\d{3})(\\\\d{3})","$1 $2 $3","[23][3-7]|[7-9]",null],["(2\\\\d{2})(\\\\d{3})","$1 $2","2[3-6]",null]]]',
"232": '["SL","00","0",null,null,"($NP$FG)","\\\\d{6,8}","[2-9]\\\\d{7}",[["(\\\\d{2})(\\\\d{6})","$1 $2",null,null]]]',
"594": '["GF","00","0",null,null,"$NP$FG","\\\\d{9}","[56]\\\\d{8}",[["(\\\\d{3})(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4",null,null]]]',
"976": '["MN","001","0",null,null,"$NP$FG","\\\\d{6,10}","[12]\\\\d{7,9}|[57-9]\\\\d{7}",[["([12]\\\\d)(\\\\d{2})(\\\\d{4})","$1 $2 $3","[12]1",null],["([12]2\\\\d)(\\\\d{5,6})","$1 $2","[12]2[1-3]",null],["([12]\\\\d{3})(\\\\d{5})","$1 $2","[12](?:27|[3-5])",null],["(\\\\d{4})(\\\\d{4})","$1 $2","[57-9]","$FG"],["([12]\\\\d{4})(\\\\d{4,5})","$1 $2","[12](?:27|[3-5])",null]]]',
"20": '["EG","00","0",null,null,"$NP$FG","\\\\d{5,10}","1\\\\d{4,9}|[2456]\\\\d{8}|3\\\\d{7}|[89]\\\\d{8,9}",[["(\\\\d)(\\\\d{7,8})","$1 $2","[23]",null],["(\\\\d{3})(\\\\d{3})(\\\\d{4})","$1 $2 $3","1[012]|[89]00",null],["(\\\\d{2})(\\\\d{6,7})","$1 $2","1[35]|[4-6]|[89][2-9]",null]]]',
"689": '["PF","00",null,null,null,null,"\\\\d{6}(?:\\\\d{2})?","4\\\\d{5,7}|8\\\\d{7}",[["(\\\\d{2})(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4","4[09]|8[79]",null],["(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2 $3","44",null]]]',
"56": '["CL","(?:0|1(?:1[0-69]|2[0-57]|5[13-58]|69|7[0167]|8[018]))0","0","0|(1(?:1[0-69]|2[0-57]|5[13-58]|69|7[0167]|8[018]))",null,"$NP$FG","\\\\d{7,11}","(?:[2-9]|600|123)\\\\d{7,8}",[["(\\\\d)(\\\\d{4})(\\\\d{4})","$1 $2 $3","2[23]","($FG)"],["(\\\\d{2})(\\\\d{3})(\\\\d{4})","$1 $2 $3","[357]|4[1-35]|6[13-57]","($FG)"],["(9)(\\\\d{4})(\\\\d{4})","$1 $2 $3","9",null],["(44)(\\\\d{3})(\\\\d{4})","$1 $2 $3","44",null],["([68]00)(\\\\d{3})(\\\\d{3,4})","$1 $2 $3","60|8","$FG"],["(600)(\\\\d{3})(\\\\d{2})(\\\\d{3})","$1 $2 $3 $4","60","$FG"],["(1230)(\\\\d{3})(\\\\d{4})","$1 $2 $3","1","$FG"],["(\\\\d{5})(\\\\d{4})","$1 $2","219","($FG)"],["(\\\\d{4,5})","$1","[1-9]","$FG","NA"]]]',
"596": '["MQ","00","0",null,null,"$NP$FG","\\\\d{9}","[56]\\\\d{8}",[["(\\\\d{3})(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4",null,null]]]',
"508": '["PM","00","0",null,null,"$NP$FG","\\\\d{6}","[45]\\\\d{5}",[["([45]\\\\d)(\\\\d{2})(\\\\d{2})","$1 $2 $3",null,null]]]',
"269": '["KM","00",null,null,null,null,"\\\\d{7}","[3478]\\\\d{6}",[["(\\\\d{3})(\\\\d{2})(\\\\d{2})","$1 $2 $3",null,null]]]',
"358": ['["FI","00|99(?:[02469]|5(?:11|33|5[59]|88|9[09]))","0",null,null,"$NP$FG","\\\\d{5,12}","1\\\\d{4,11}|[2-9]\\\\d{4,10}",[["(\\\\d{3})(\\\\d{3,7})","$1 $2","(?:[1-3]00|[6-8]0)",null],["(116\\\\d{3})","$1","116","$FG"],["(\\\\d{2})(\\\\d{4,10})","$1 $2","[14]|2[09]|50|7[135]",null],["(\\\\d)(\\\\d{4,11})","$1 $2","[25689][1-8]|3",null]]]','["AX","00|99(?:[02469]|5(?:11|33|5[59]|88|9[09]))","0",null,null,"$NP$FG","\\\\d{5,12}","1\\\\d{5,11}|[35]\\\\d{5,9}|[27]\\\\d{4,9}|4\\\\d{5,10}|6\\\\d{7,9}|8\\\\d{6,9}"]'],
"251": '["ET","00","0",null,null,"$NP$FG","\\\\d{7,9}","[1-59]\\\\d{8}",[["([1-59]\\\\d)(\\\\d{3})(\\\\d{4})","$1 $2 $3",null,null]]]',
"681": '["WF","00",null,null,null,null,"\\\\d{6}","[4-8]\\\\d{5}",[["(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2 $3",null,null]]]',
"853": '["MO","00",null,null,null,null,"\\\\d{8}","[268]\\\\d{7}",[["([268]\\\\d{3})(\\\\d{4})","$1 $2",null,null]]]',
"44": ['["GB","00","0",null,null,"$NP$FG","\\\\d{4,10}","\\\\d{7,10}",[["(7\\\\d{3})(\\\\d{6})","$1 $2","7(?:[1-5789]|62)",null],["(\\\\d{2})(\\\\d{4})(\\\\d{4})","$1 $2 $3","2|5[56]|7[06]",null],["(\\\\d{3})(\\\\d{3})(\\\\d{4})","$1 $2 $3","1(?:1|\\\\d1)|3|9[018]",null],["(\\\\d{5})(\\\\d{4,5})","$1 $2","1(?:38|5[23]|69|76|94)",null],["(1\\\\d{3})(\\\\d{5,6})","$1 $2","1",null],["(800)(\\\\d{4})","$1 $2","800",null],["(845)(46)(4\\\\d)","$1 $2 $3","845",null],["(8\\\\d{2})(\\\\d{3})(\\\\d{4})","$1 $2 $3","8(?:4[2-5]|7[0-3])",null],["(80\\\\d)(\\\\d{3})(\\\\d{4})","$1 $2 $3","80",null],["([58]00)(\\\\d{6})","$1 $2","[58]00",null]]]','["GG","00","0",null,null,"$NP$FG","\\\\d{6,10}","[135789]\\\\d{6,9}"]','["IM","00","0",null,null,"$NP$FG","\\\\d{6,10}","[135789]\\\\d{6,9}"]','["JE","00","0",null,null,"$NP$FG","\\\\d{6,10}","[135789]\\\\d{6,9}"]'],
"244": '["AO","00",null,null,null,null,"\\\\d{9}","[29]\\\\d{8}",[["(\\\\d{3})(\\\\d{3})(\\\\d{3})","$1 $2 $3",null,null]]]',
"211": '["SS","00","0",null,null,null,"\\\\d{9}","[19]\\\\d{8}",[["(\\\\d{3})(\\\\d{3})(\\\\d{3})","$1 $2 $3",null,"$NP$FG"]]]',
"373": '["MD","00","0",null,null,"$NP$FG","\\\\d{8}","[235-9]\\\\d{7}",[["(\\\\d{2})(\\\\d{3})(\\\\d{3})","$1 $2 $3","22|3",null],["([25-7]\\\\d{2})(\\\\d{2})(\\\\d{3})","$1 $2 $3","2[13-9]|[5-7]",null],["([89]\\\\d{2})(\\\\d{5})","$1 $2","[89]",null]]]',
"996": '["KG","00","0",null,null,"$NP$FG","\\\\d{5,10}","[235-8]\\\\d{8,9}",[["(\\\\d{3})(\\\\d{3})(\\\\d{3})","$1 $2 $3","[25-7]|31[25]",null],["(\\\\d{4})(\\\\d{5})","$1 $2","3(?:1[36]|[2-9])",null],["(\\\\d{3})(\\\\d{3})(\\\\d)(\\\\d{3})","$1 $2 $3 $4","8",null]]]',
"93": '["AF","00","0",null,null,"$NP$FG","\\\\d{7,9}","[2-7]\\\\d{8}",[["([2-7]\\\\d)(\\\\d{3})(\\\\d{4})","$1 $2 $3","[2-7]",null]]]',
"260": '["ZM","00","0",null,null,"$NP$FG","\\\\d{9}","[289]\\\\d{8}",[["([29]\\\\d)(\\\\d{7})","$1 $2","[29]",null],["(800)(\\\\d{3})(\\\\d{3})","$1 $2 $3","8",null]]]',
"378": '["SM","00",null,"(?:0549)?([89]\\\\d{5})","0549$1",null,"\\\\d{6,10}","[05-7]\\\\d{7,9}",[["(\\\\d{2})(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4","[5-7]",null],["(0549)(\\\\d{6})","$1 $2","0",null,"($1) $2"],["(\\\\d{6})","0549 $1","[89]",null,"(0549) $1"]]]',
"235": '["TD","00|16",null,null,null,null,"\\\\d{8}","[2679]\\\\d{7}",[["(\\\\d{2})(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4",null,null]]]',
"960": '["MV","0(?:0|19)",null,null,null,null,"\\\\d{7,10}","[346-8]\\\\d{6,9}|9(?:00\\\\d{7}|\\\\d{6})",[["(\\\\d{3})(\\\\d{4})","$1-$2","[3467]|9(?:[1-9]|0[1-9])",null],["(\\\\d{3})(\\\\d{3})(\\\\d{4})","$1 $2 $3","[89]00",null]]]',
"221": '["SN","00",null,null,null,null,"\\\\d{9}","[3789]\\\\d{8}",[["(\\\\d{2})(\\\\d{3})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4","[379]",null],["(\\\\d{3})(\\\\d{2})(\\\\d{2})(\\\\d{2})","$1 $2 $3 $4","8",null]]]',
"595": '["PY","00","0",null,null,null,"\\\\d{5,9}","5[0-5]\\\\d{4,7}|[2-46-9]\\\\d{5,8}",[["(\\\\d{2})(\\\\d{5})","$1 $2","(?:[26]1|3[289]|4[124678]|7[123]|8[1236])","($NP$FG)"],["(\\\\d{2})(\\\\d{3})(\\\\d{3,4})","$1 $2 $3","(?:[26]1|3[289]|4[124678]|7[123]|8[1236])","($NP$FG)"],["(\\\\d{3})(\\\\d{3,6})","$1 $2","[2-9]0","$NP$FG"],["(\\\\d{3})(\\\\d{6})","$1 $2","9[1-9]","$NP$FG"],["(\\\\d{2})(\\\\d{3})(\\\\d{4})","$1 $2 $3","8700",null],["(\\\\d{3})(\\\\d{4,5})","$1 $2","[2-8][1-9]","($NP$FG)"],["(\\\\d{3})(\\\\d{3})(\\\\d{3})","$1 $2 $3","[2-8][1-9]","$NP$FG"]]]',
"977": '["NP","00","0",null,null,"$NP$FG","\\\\d{6,10}","[1-8]\\\\d{7}|9(?:[1-69]\\\\d{6,8}|7[2-6]\\\\d{5,7}|8\\\\d{8})",[["(1)(\\\\d{7})","$1-$2","1[2-6]",null],["(\\\\d{2})(\\\\d{6})","$1-$2","1[01]|[2-8]|9(?:[1-69]|7[15-9])",null],["(9\\\\d{2})(\\\\d{7})","$1-$2","9(?:6[013]|7[245]|8)","$FG"]]]',
"36": '["HU","00","06",null,null,"($FG)","\\\\d{6,9}","[1-9]\\\\d{7,8}",[["(1)(\\\\d{3})(\\\\d{4})","$1 $2 $3","1",null],["(\\\\d{2})(\\\\d{3})(\\\\d{3,4})","$1 $2 $3","[2-9]",null]]]',
};
PK
!<òY.ýtt5chrome/res/phonenumberutils/PhoneNumberNormalizer.jsm/* This Source Code Form is subject to the terms of the Apache License, Version
 * 2.0. If a copy of the Apache License was not distributed with this file, You
 * can obtain one at https://www.apache.org/licenses/LICENSE-2.0 */

// This library came from https://github.com/andreasgal/PhoneNumber.js but will
// be further maintained by our own in Form Autofill codebase.

"use strict";

var EXPORTED_SYMBOLS = ["PhoneNumberNormalizer"];

var PhoneNumberNormalizer = (function() {
  const UNICODE_DIGITS = /[\uFF10-\uFF19\u0660-\u0669\u06F0-\u06F9]/g;
  const VALID_ALPHA_PATTERN = /[a-zA-Z]/g;
  const LEADING_PLUS_CHARS_PATTERN = /^[+\uFF0B]+/g;
  const NON_DIALABLE_CHARS = /[^,#+\*\d]/g;

  // Map letters to numbers according to the ITU E.161 standard
  let E161 = {
    "a": 2, "b": 2, "c": 2,
    "d": 3, "e": 3, "f": 3,
    "g": 4, "h": 4, "i": 4,
    "j": 5, "k": 5, "l": 5,
    "m": 6, "n": 6, "o": 6,
    "p": 7, "q": 7, "r": 7, "s": 7,
    "t": 8, "u": 8, "v": 8,
    "w": 9, "x": 9, "y": 9, "z": 9,
  };

  // Normalize a number by converting unicode numbers and symbols to their
  // ASCII equivalents and removing all non-dialable characters.
  function NormalizeNumber(number, numbersOnly) {
    if (typeof number !== "string") {
      return "";
    }

    number = number.replace(UNICODE_DIGITS,
                            function(ch) {
                              return String.fromCharCode(48 + (ch.charCodeAt(0) & 0xf));
                            });
    if (!numbersOnly) {
      number = number.replace(VALID_ALPHA_PATTERN,
                              function(ch) {
                                return String(E161[ch.toLowerCase()] || 0);
                              });
    }
    number = number.replace(LEADING_PLUS_CHARS_PATTERN, "+");
    number = number.replace(NON_DIALABLE_CHARS, "");
    return number;
  }


  return {
    Normalize: NormalizeNumber,
  };
})();
PK
!<2W˜Þ99*en-US/locale/en-US/formautofill.properties
saveAddressesMessage = %S now saves addresses so you can fill out forms faster.
autofillOptionsLink = Form Autofill Options
autofillOptionsLinkOSX = Form Autofill Preferences
autofillSecurityOptionsLink = Form Autofill & Security Options
autofillSecurityOptionsLinkOSX = Form Autofill & Security Preferences
changeAutofillOptions = Change Form Autofill Options
changeAutofillOptionsOSX = Change Form Autofill Preferences
changeAutofillOptionsAccessKey = C
addressesSyncCheckbox = Share addresses with synced devices
creditCardsSyncCheckbox = Share credit cards with synced devices
updateAddressMessage = Would you like to update your address with this new information?
updateAddressDescriptionLabel = Address to update:
createAddressLabel = Create New Address
createAddressAccessKey = C
updateAddressLabel = Update Address
updateAddressAccessKey = U
saveCreditCardMessage = Would you like %S to save this credit card? (Security code will not be saved)
saveCreditCardDescriptionLabel = Credit card to save:
saveCreditCardLabel = Save Credit Card
saveCreditCardAccessKey = S
cancelCreditCardLabel = Don’t Save
cancelCreditCardAccessKey = D
neverSaveCreditCardLabel = Never Save Credit Cards
neverSaveCreditCardAccessKey = N
updateCreditCardMessage = Would you like to update your credit card with this new information?
updateCreditCardDescriptionLabel = Credit card to update:
createCreditCardLabel = Create New Credit Card
createCreditCardAccessKey = C
updateCreditCardLabel = Update Credit Card
updateCreditCardAccessKey = U
openAutofillMessagePanel = Open Form Autofill message panel

autocompleteFooterOptionShort = More Options
autocompleteFooterOptionOSXShort = Preferences
category.address = address
category.name = name
category.organization2 = organization
category.tel = phone
category.email = email
fieldNameSeparator = ,\u0020
phishingWarningMessage = Also autofills %S
phishingWarningMessage2 = Autofills %S
insecureFieldWarningDescription = %S has detected an insecure site. Form Autofill is temporarily disabled.
clearFormBtnLabel2 = Clear Autofill Form

autofillHeader = Forms & Autofill
autofillDescription = %S can save your address and credit card information.
autofillAddressesCheckbox = Autofill addresses
learnMoreLabel = Learn more
savedAddressesBtnLabel = Saved Addresses…
autofillCreditCardsCheckbox = Autofill credit cards
savedCreditCardsBtnLabel = Saved Credit Cards…

manageAddressesTitle = Saved Addresses
manageCreditCardsTitle = Saved Credit Cards
addressesListHeader = Addresses
creditCardsListHeader = Credit Cards
removeBtnLabel = Remove
addBtnLabel = Add…
editBtnLabel = Edit…
manageDialogsWidth = 560px

addNewAddressTitle = Add New Address
editAddressTitle = Edit Address
givenName = First Name
additionalName = Middle Name
familyName = Last Name
organization2 = Organization
streetAddress = Street Address

neighborhood = Neighborhood
village_township = Village or Township
island = Island
townland = Townland

city = City
district = District
post_town = Post town
suburb = Suburb

province = Province
state = State
county = County
parish = Parish
prefecture = Prefecture
area = Area
do_si = Do/Si
department = Department
emirate = Emirate
oblast = Oblast

pin = Pin
postalCode = Postal Code
zip = ZIP Code
eircode = Eircode

country = Country or Region
tel = Phone
email = Email
cancelBtnLabel = Cancel
saveBtnLabel = Save
countryWarningMessage2 = Form Autofill is currently available only for certain countries.

addNewCreditCardTitle = Add New Credit Card
editCreditCardTitle = Edit Credit Card
cardNumber = Card Number
invalidCardNumber = Please enter a valid card number
nameOnCard = Name on Card
cardExpiresMonth = Exp. Month
cardExpiresYear = Exp. Year
billingAddress = Billing Address
cardNetwork = Card Type
cardCVV = CVV

cardNetwork.amex = American Express
cardNetwork.cartebancaire = Carte Bancaire
cardNetwork.diners = Diners Club
cardNetwork.discover = Discover
cardNetwork.jcb = JCB
cardNetwork.mastercard = MasterCard
cardNetwork.mir = MIR
cardNetwork.unionpay = Union Pay
cardNetwork.visa = Visa

editCreditCardPasswordPrompt.win = %S is trying to show credit card information. Confirm access to this Windows account below.
editCreditCardPasswordPrompt.macosx = %S is trying to show credit card information.
editCreditCardPasswordPrompt.linux = %S is trying to show credit card information.
useCreditCardPasswordPrompt.win = %S is trying to use stored credit card information. Confirm access to this Windows account below.
useCreditCardPasswordPrompt.macosx = %S is trying to use stored credit card information.
useCreditCardPasswordPrompt.linux = %S is trying to use stored credit card information.
PK
!<¦ß~‹ªª
manifest.json{
  "manifest_version": 2,
  "name": "Form Autofill",
  "version": "1.0",

  "applications": {
    "gecko": {
      "id": "formautofill@mozilla.org"
    }
  },

  "background": {
    "scripts": ["background.js"]
  },

  "experiment_apis": {
    "formautofill": {
      "schema": "schema.json",
      "parent": {
        "scopes": ["addon_parent"],
        "script": "api.js",
        "events": ["startup"]
      }
    }
  }
}
PK
!<DÒhpschema.json[]
PK==n