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.manifestlocale formautofill en-US en-US/locale/en-US/
PK
!<H>÷ߤ¤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();

const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { XPCOMUtils } = 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);
    Services.mm.loadFrameScript(
      "chrome://formautofill/content/FormAutofillFrameScript.js",
      true,
      true
    );
  }

  onShutdown(isAppShutdown) {
    if (isAppShutdown) {
      return;
    }

    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
!<¿C›……
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.
  // Note that if we ever wanted to change this, we should make
  // sure we manually invalidate the startup cache using the
  // startupcache-invalidate notification.
});
PK
!<Ëû¯*)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 */

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

/**
 * Handles content's interactions for the frame.
 */
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);
    addEventListener("DOMFormBeforeSubmit", 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;
    }

    switch (evt.type) {
      case "focusin": {
        this.onFocusIn(evt);
        break;
      }
      case "DOMFormBeforeSubmit": {
        this.onDOMFormBeforeSubmit(evt);
        break;
      }
      default: {
        throw new Error("Unexpected event type");
      }
    }
  },

  onFocusIn(evt) {
    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();
  },

  /**
   * Handle the DOMFormBeforeSubmit event.
   * @param {Event} evt
   */
  onDOMFormBeforeSubmit(evt) {
    let formElement = evt.target;

    if (!FormAutofill.isAutofillEnabled) {
      return;
    }

    FormAutofillContent.formSubmitted(formElement);
  },

  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
!<üBõNõN#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();
    } else {
      for (let field of this._elements.form.elements) {
        this.updatePopulatedState(field);
        this.updateCustomValidity(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());
  }

  /**
   * Run custom validity routines specific to the field and type of form.
   *
   * @param {DOMElement} field The field that will be validated.
   */
  updateCustomValidity(field) {}
}

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 {function} config.findAddressSelectOption Finds the matching select option for a given
                                                      select element, address, and fieldName.
   * @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,
      };
    }

    let { addressLevel1Options } = this.getFormFormat(record.country);
    this.populateAddressLevel1(addressLevel1Options, record.country);

    super.loadRecord(record);
    this.loadAddressLevel1(record["address-level1"], record.country);
    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,
      addressLevel1Options,
      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);
    this.populateAddressLevel1(addressLevel1Options, country);
  }

  /**
   * 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");
    }
  }

  /**
   * Set the address-level1 value on the form field (input or select, whichever is present).
   *
   * @param {string} addressLevel1Value Value of the address-level1 from the autofill record
   * @param {string} country The corresponding country
   */
  loadAddressLevel1(addressLevel1Value, country) {
    let field = this._elements.form.querySelector("#address-level1");

    if (field.localName == "input") {
      field.value = addressLevel1Value || "";
      return;
    }

    let matchedSelectOption = this.findAddressSelectOption(
      field,
      {
        country,
        "address-level1": addressLevel1Value,
      },
      "address-level1"
    );
    if (matchedSelectOption && !matchedSelectOption.selected) {
      field.value = matchedSelectOption.value;
      field.dispatchEvent(new Event("input", { bubbles: true }));
      field.dispatchEvent(new Event("change", { bubbles: true }));
    } else if (addressLevel1Value) {
      // If the option wasn't found, insert an option at the beginning of
      // the select that matches the stored value.
      field.insertBefore(
        new Option(addressLevel1Value, addressLevel1Value, true, true),
        field.firstChild
      );
    }
  }

  /**
   * Replace the text input for address-level1 with a select dropdown if
   * a fixed set of names exists. Otherwise show a text input.
   *
   * @param {Map?} options Map of options with regionCode -> name mappings
   * @param {string} country The corresponding country
   */
  populateAddressLevel1(options, country) {
    let field = this._elements.form.querySelector("#address-level1");

    if (field.dataset.country == country) {
      return;
    }

    if (!options) {
      if (field.localName == "input") {
        return;
      }

      let input = document.createElement("input");
      input.setAttribute("type", "text");
      input.id = "address-level1";
      input.required = field.required;
      input.disabled = field.disabled;
      input.tabIndex = field.tabIndex;
      field.replaceWith(input);
      return;
    }

    if (field.localName == "input") {
      let select = document.createElement("select");
      select.id = "address-level1";
      select.required = field.required;
      select.disabled = field.disabled;
      select.tabIndex = field.tabIndex;
      field.replaceWith(select);
      field = select;
    }

    field.textContent = "";
    field.dataset.country = country;
    let fragment = document.createDocumentFragment();
    fragment.appendChild(new Option(undefined, undefined, true, true));
    for (let [regionCode, regionName] of options) {
      let option = new Option(regionName, regionCode);
      fragment.appendChild(option);
    }
    field.appendChild(fragment);
  }

  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.attachEventListeners();
    this.loadRecord(record, addresses);
  }

  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);
    }
  }

  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();
  }

  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);
  }

  updateCustomValidity(field) {
    super.updateCustomValidity(field);

    // Mark the cc-number field as invalid if the number is empty or invalid.
    if (field == this._elements.ccNumber && !this.isCCNumber(field.value)) {
      let invalidCardNumberString = this._elements
        .invalidCardNumberStringElement.textContent;
      field.setCustomValidity(invalidCardNumberString || " ");
    }
  }
}
PK
!<ŸXª*¼0¼0 chrome/content/customElements.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/. */

// This file is loaded into the browser window scope.
/* eslint-env mozilla/browser-window */
/* eslint-disable mozilla/balanced-listeners */ // Not relevant since the document gets unloaded.

"use strict";

// Wrap in a block to prevent leaking to window scope.
(() => {
  const { Services } = ChromeUtils.import(
    "resource://gre/modules/Services.jsm"
  );

  class MozAutocompleteProfileListitemBase extends MozElements.MozRichlistitem {
    constructor() {
      super();

      /**
       * 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
       */
      this.selectedByMouseOver = true;
    }

    get _stringBundle() {
      if (!this.__stringBundle) {
        this.__stringBundle = Services.strings.createBundle(
          "chrome://formautofill/locale/formautofill.properties"
        );
      }
      return this.__stringBundle;
    }

    _cleanup() {
      this.removeAttribute("formautofillattached");
      if (this._itemBox) {
        this._itemBox.removeAttribute("size");
      }
    }

    _onOverflow() {}

    _onUnderflow() {}

    handleOverUnderflow() {}

    _adjustAutofillItemLayout() {
      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");
      }
    }
  }

  MozElements.MozAutocompleteProfileListitem = class MozAutocompleteProfileListitem extends MozAutocompleteProfileListitemBase {
    connectedCallback() {
      if (this.delayConnectedCallback()) {
        return;
      }

      this.textContent = "";

      this.appendChild(
        MozXULElement.parseXULToFragment(`
        <div xmlns="http://www.w3.org/1999/xhtml" class="autofill-item-box">
          <div class="profile-label-col profile-item-col">
            <span class="profile-label-affix"></span>
            <span class="profile-label"></span>
          </div>
          <div class="profile-comment-col profile-item-col">
            <span class="profile-comment"></span>
          </div>
        </div>
      `)
      );

      this._itemBox = this.querySelector(".autofill-item-box");
      this._labelAffix = this.querySelector(".profile-label-affix");
      this._label = this.querySelector(".profile-label");
      this._comment = this.querySelector(".profile-comment");

      this.initializeAttributeInheritance();
      this._adjustAcItem();
    }

    static get inheritedAttributes() {
      return {
        ".autofill-item-box": "ac-image",
      };
    }

    set selected(val) {
      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;
    }

    get selected() {
      return this.getAttribute("selected") == "true";
    }

    _adjustAcItem() {
      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;
    }
  };

  customElements.define(
    "autocomplete-profile-listitem",
    MozElements.MozAutocompleteProfileListitem,
    { extends: "richlistitem" }
  );

  class MozAutocompleteProfileListitemFooter extends MozAutocompleteProfileListitemBase {
    constructor() {
      super();

      this.addEventListener("click", event => {
        if (event.button != 0) {
          return;
        }

        if (this._warningTextBox.contains(event.originalTarget)) {
          return;
        }

        window.openPreferences("privacy-form-autofill");
      });
    }

    connectedCallback() {
      if (this.delayConnectedCallback()) {
        return;
      }

      this.textContent = "";
      this.appendChild(
        MozXULElement.parseXULToFragment(`
        <div xmlns="http://www.w3.org/1999/xhtml" class="autofill-item-box autofill-footer">
          <div class="autofill-footer-row autofill-warning"></div>
          <div class="autofill-footer-row autofill-button"></div>
        </div>
      `)
      );

      this._itemBox = this.querySelector(".autofill-footer");
      this._optionButton = this.querySelector(".autofill-button");
      this._warningTextBox = this.querySelector(".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]
        );
        this.parentNode.parentNode.adjustHeight();
      };

      this._adjustAcItem();
    }

    _onCollapse() {
      /* global messageManager */
      if (this.showWarningText) {
        messageManager.removeMessageListener(
          "FormAutofill:UpdateWarningMessage",
          this._updateWarningNote
        );
      }
      this._itemBox.removeAttribute("no-warning");
    }

    _adjustAcItem() {
      /* 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");
      }
    }
  }

  customElements.define(
    "autocomplete-profile-listitem-footer",
    MozAutocompleteProfileListitemFooter,
    { extends: "richlistitem" }
  );

  class MozAutocompleteCreditcardInsecureField extends MozAutocompleteProfileListitemBase {
    connectedCallback() {
      if (this.delayConnectedCallback()) {
        return;
      }
      this.textContent = "";
      this.appendChild(
        MozXULElement.parseXULToFragment(`
        <div xmlns="http://www.w3.org/1999/xhtml" class="autofill-insecure-item"></div>
      `)
      );

      this._itemBox = this.querySelector(".autofill-insecure-item");

      this._adjustAcItem();
    }

    set selected(val) {
      // Make this item unselectable since we see this item as a pure message.
      return false;
    }

    get selected() {
      return this.getAttribute("selected") == "true";
    }

    _adjustAcItem() {
      this._adjustAutofillItemLayout();
      this.setAttribute("formautofillattached", "true");

      let value = this.getAttribute("ac-value");
      this._itemBox.textContent = value;
    }
  }

  customElements.define(
    "autocomplete-creditcard-insecure-field",
    MozAutocompleteCreditcardInsecureField,
    { extends: "richlistitem" }
  );

  class MozAutocompleteProfileListitemClearButton extends MozAutocompleteProfileListitemBase {
    constructor() {
      super();

      this.addEventListener("click", event => {
        if (event.button != 0) {
          return;
        }

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

    connectedCallback() {
      if (this.delayConnectedCallback()) {
        return;
      }

      this.textContent = "";
      this.appendChild(
        MozXULElement.parseXULToFragment(`
        <div xmlns="http://www.w3.org/1999/xhtml" class="autofill-item-box autofill-footer">
          <div class="autofill-footer-row autofill-button"></div>
        </div>
      `)
      );

      this._itemBox = this.querySelector(".autofill-item-box");
      this._clearBtn = this.querySelector(".autofill-button");

      this._adjustAcItem();
    }

    _adjustAcItem() {
      this._adjustAutofillItemLayout();
      this.setAttribute("formautofillattached", "true");

      let clearFormBtnLabel = this._stringBundle.GetStringFromName(
        "clearFormBtnLabel2"
      );
      this._clearBtn.textContent = clearFormBtnLabel;
    }
  }

  customElements.define(
    "autocomplete-profile-listitem-clear-button",
    MozAutocompleteProfileListitemClearButton,
    { extends: "richlistitem" }
  );
})();
PK
!<r<ÓÆÆ 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">
      <!-- The address-level1 input will get replaced by a select dropdown
           by autofillEditForms.js when the selected country has provided
           specific options. -->
      <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" data-localization="saveBtnLabel"/>
    <span id="country-warning-message" data-localization="countryWarningMessage2"/>
  </div>
  <script><![CDATA[
    "use strict";

    /* import-globals-from l10n.js */

    let {
      DEFAULT_REGION,
      countries,
    } = FormAutofill;
    let {
      getFormFormat,
      findAddressSelectOption,
    } = 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),
      findAddressSelectOption: findAddressSelectOption.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
!<- É*€€#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" data-localization="saveBtnLabel"/>
  </div>
  <script><![CDATA[
    "use strict";

    /* import-globals-from l10n.js */

    (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
!<no§@@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
const { FormAutofill } = ChromeUtils.import(
  "resource://formautofill/FormAutofill.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.updateSaveButtonState();
    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) {
    this.updateSaveButtonState();
  }

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

  updateSaveButtonState() {
    // 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");
    }
  }

  /**
   * 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
!<*1--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;
  -moz-binding: none;
}

/* 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[resultstyles~="autofill-profile"] {
  min-width: 150px !important;
}

#PopupAutoComplete[resultstyles~="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
!<¡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
!<éF2	qqchrome/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 */

const { FormAutofillUtils } = 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
!<`4A@@$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>
    "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
!<f+UDNN&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>
    "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
!<ŠG$æ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: var(--in-content-box-background-odd);
}

#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
!<`ehYÒ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";

const { AppConstants } = ChromeUtils.import(
  "resource://gre/modules/AppConstants.jsm"
);
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { XPCOMUtils } = ChromeUtils.import(
  "resource://gre/modules/XPCOMUtils.jsm"
);
const { FormAutofill } = 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]
  );
});

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
!<ï(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"];

const { XPCOMUtils } = ChromeUtils.import(
  "resource://gre/modules/XPCOMUtils.jsm"
);
const { Services } = 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(",")
);

// XXX: This should be invalidated on intl:app-locales-changed.
XPCOMUtils.defineLazyGetter(FormAutofill, "countries", () => {
  let availableRegionCodes = Services.intl.getAvailableLocaleDisplayNames(
    "region"
  );
  let displayNames = Services.intl.getRegionDisplayNames(
    undefined,
    availableRegionCodes
  );
  let result = new Map();
  for (let i = 0; i < availableRegionCodes.length; i++) {
    result.set(availableRegionCodes[i].toUpperCase(), displayNames[i]);
  }
  return result;
});
PK
!<Ÿϖ!9\9\"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;

const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { XPCOMUtils } = 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 isFormAutofillSearch = true;
    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)
    ) {
      isFormAutofillSearch = false;
      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);
      // Don't save cache results or reset state when returning non-autofill results such as the
      // form history fallback above.
      if (isFormAutofillSearch) {
        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();
      } else {
        // Clear the cache so that we don't try to autofill from it after falling
        // back to form history.
        ProfileAutocomplete.lastProfileAutoCompleteResult = null;
      }
    });
  },

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

    this.debug(
      "ensureRegistered. Finished with _registered:",
      this._registered
    );
  },

  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 = {
  /**
   * @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.
   */
  get savedFieldNames() {
    return Services.cpmm.sharedData.get("FormAutofill:savedFieldNames");
  },

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

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

    // eslint-disable-next-line mozilla/balanced-listeners
    Services.cpmm.sharedData.addEventListener("change", this);

    let autofillEnabled = Services.cpmm.sharedData.get("FormAutofill:enabled");
    // 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();
    }
  },

  /**
   * 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 a form submission 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 submit event.
   * @param {Window} domWin Content window only passed for unit tests
   */
  formSubmitted(formElement, domWin = formElement.ownerGlobal) {
    this.debug("Handling form submission");

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

    // The `domWin` truthiness test is used by unit tests to bypass this check.
    if (domWin && PrivateBrowsingUtils.isContentWindowPrivate(domWin)) {
      this.debug("Ignoring submission in a private window");
      return;
    }

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

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

    this._onFormSubmit(records, domWin, handler.timeStartedFillingMS);
  },

  handleEvent(evt) {
    switch (evt.type) {
      case "change": {
        if (!evt.changedKeys.includes("FormAutofill:enabled")) {
          return;
        }
        if (Services.cpmm.sharedData.get("FormAutofill:enabled")) {
          ProfileAutocomplete.ensureRegistered();
        } else {
          ProfileAutocomplete.ensureUnregistered();
        }
        break;
      }
    }
  },

  /**
   * 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
!<%ަ9=9=%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"];

const { AppConstants } = ChromeUtils.import(
  "resource://gre/modules/AppConstants.jsm"
);
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { FormAutofill } = ChromeUtils.import(
  "resource://formautofill/FormAutofill.jsm"
);
const { FormAutofillUtils } = 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]),
    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]),
    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", {
      is: "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.appendNotificationContent(notificationContent);
        }
        this._updateDescription(notificationContent, description);
      };
      this._setAnchor(browser, anchor);
      chromeWin.PopupNotifications.show(
        browser,
        notificationId,
        message,
        anchor.id,
        ...this._createActions(mainAction, secondaryActions, resolve),
        options
      );
    });
  },
};
PK
!<™•".Œ.Œ"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"];

const { AppConstants } = ChromeUtils.import(
  "resource://gre/modules/AppConstants.jsm"
);
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { XPCOMUtils } = ChromeUtils.import(
  "resource://gre/modules/XPCOMUtils.jsm"
);
const { FormAutofill } = 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]
  );
});

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
!<˜r¥­””%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"];

const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { XPCOMUtils } = ChromeUtils.import(
  "resource://gre/modules/XPCOMUtils.jsm"
);
const { FormAutofill } = 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$/gi)) {
      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]|$)/gi
      ) ||
      this._matchRegexp(
        element,
        /(?:exp.*date[^y\\n\\r]*|mm\\s*[-/]?\\s*)yyyy(?:[^y]|$)/gi
      )
    ) {
      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);
  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
!<›Sl*l*$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";

var EXPORTED_SYMBOLS = ["FormAutofillNameUtils"];

// FormAutofillNameUtils is initially translated from
// https://cs.chromium.org/chromium/src/components/autofill/core/browser/autofill_data_util.cc?rcl=b861deff77abecff11ae6a9f6946e9cc844b9817
var FormAutofillNameUtils = {
  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: [
    "강전",
    "남궁",
    "독고",
    "동방",
    "망절",
    "사공",
    "서문",
    "선우",
    "소봉",
    "어금",
    "장곡",
    "제갈",
    "황목",
    "황보",
  ],

  // 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;
    }
    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
!<ºR/|b|b!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"];

const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { XPCOMUtils } = ChromeUtils.import(
  "resource://gre/modules/XPCOMUtils.jsm"
);

const { FormAutofill } = 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, "privacy-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);
    }

    for (let win of Services.wm.getEnumerator("navigator:browser")) {
      this.injectElements(win.document);
    }
    Services.wm.addListener(this);
  },

  injectElements(doc) {
    Services.scriptloader.loadSubScript(
      "chrome://formautofill/content/customElements.js",
      doc.ownerGlobal
    );
  },

  onOpenWindow(xulWindow) {
    const win = xulWindow.docShell.domWindow;
    win.addEventListener(
      "load",
      () => {
        if (
          win.document.documentElement.getAttribute("windowtype") ==
          "navigator:browser"
        ) {
          this.injectElements(win.document);
        }
      },
      { once: true }
    );
  },

  onCloseWindow() {},

  observe(subject, topic, data) {
    log.debug("observe:", topic, "with data:", data);
    switch (topic) {
      case "privacy-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.sharedData.set("FormAutofill:enabled", this._active);
    // Sync autofill enabled to make sure the value is up-to-date
    // no matter when the new content process is initialized.
    Services.ppmm.sharedData.flush();
  },

  /**
   * 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.sharedData.get(
      "FormAutofill:savedFieldNames"
    );

    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");
        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, "privacy-pane-loaded");
    Services.prefs.removeObserver(ENABLED_AUTOFILL_ADDRESSES_PREF, this);
    Services.wm.removeListener(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");

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

    Services.ppmm.sharedData.set(
      "FormAutofill:savedFieldNames",
      savedFieldNames
    );
    Services.ppmm.sharedData.flush();

    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");
        };
      } 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
!<NHþ;p(p(&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";

const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { FormAutofill } = ChromeUtils.import(
  "resource://formautofill/FormAutofill.jsm"
);
const { FormAutofillUtils } = 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 formAutofillGroup = document.createXULElement("vbox");
    let addressAutofill = document.createXULElement("hbox");
    let addressAutofillCheckboxGroup = document.createXULElement("hbox");
    let addressAutofillCheckbox = document.createXULElement("checkbox");
    let addressAutofillLearnMore = document.createXULElement("label", {
      is: "text-link",
    });
    let savedAddressesBtn = document.createXULElement("button", {
      is: "highlightable-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";

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

    formAutofillGroupBoxLabelHeading.textContent = this.bundle.GetStringFromName(
      "autofillHeader"
    );

    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(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", {
        is: "text-link",
      });
      let savedCreditCardsBtn = document.createXULElement("button", {
        is: "highlightable-button",
      });
      savedCreditCardsBtn.className = "accessory-button";
      creditCardAutofillCheckbox.className = "tail-with-learn-more";
      creditCardAutofillLearnMore.className = "learnMore";

      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
!<Qzd‘¬¬"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"];

const { XPCOMUtils } = ChromeUtils.import(
  "resource://gre/modules/XPCOMUtils.jsm"
);
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");

const { FormAutofill } = 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;

    let country =
      addressFound.country ||
      addressToMerge.country ||
      FormAutofill.DEFAULT_REGION;
    let collators = FormAutofillUtils.getSearchCollators(country);
    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.compareStreetAddress(
              existingField,
              incomingField,
              collators
            )
          ) {
            // Keep the street-address 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 if (
            field != "street-address" &&
            FormAutofillUtils.strCompare(
              existingField,
              incomingField,
              collators
            )
          ) {
            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
!<}f1ö®-®-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"];

const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { Changeset, Store, SyncEngine, Tracker } = ChromeUtils.import(
  "resource://services-sync/engines.js"
);
const { CryptoWrapper } = ChromeUtils.import(
  "resource://services-sync/record.js"
);
const { Utils } = ChromeUtils.import("resource://services-sync/util.js");
const { SCORE_INCREMENT_XLARGE } = ChromeUtils.import(
  "resource://services-sync/constants.js"
);

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
!<µ}B†Š†Š 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;

const { XPCOMUtils } = ChromeUtils.import(
  "resource://gre/modules/XPCOMUtils.jsm"
);
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { FormAutofill } = 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_isoids",
      "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(", ");
  },

  /**
   * Internal method to split an address to multiple parts per the provided delimiter,
   * removing blank parts.
   * @param {string} address The address the split
   * @param {string} [delimiter] The separator that is used between lines in the address
   * @returns {string[]}
   */
  _toStreetAddressParts(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);
  },

  /**
   * Converts a street address to a single line, removing linebreaks marked by the delimiter
   * @param {string} address The address the convert
   * @param {string} [delimiter] The separator that is used between lines in the address
   * @returns {string}
   */
  toOneLineAddress(address, delimiter = "\n") {
    let addressParts = this._toStreetAddressParts(address, delimiter);
    return addressParts.join(this.getAddressSeparator());
  },

  /**
   * Compares two addresses, removing internal whitespace
   * @param {string} a The first address to compare
   * @param {string} b The second address to compare
   * @param {array} collators Search collators that will be used for comparison
   * @param {string} [delimiter="\n"] The separator that is used between lines in the address
   * @returns {boolean} True if the addresses are equal, false otherwise
   */
  compareStreetAddress(a, b, collators, delimiter = "\n") {
    let oneLineA = this._toStreetAddressParts(a, delimiter)
      .map(p => p.replace(/\s/g, ""))
      .join("");
    let oneLineB = this._toStreetAddressParts(b, delimiter)
      .map(p => p.replace(/\s/g, ""))
      .join("");
    return this.strCompare(oneLineA, oneLineB, collators);
  },

  /**
   * 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);
    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.
   */
  getSearchCollators(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];
      let options = {
        ignorePunctuation: true,
        sensitivity: "base",
        usage: "search",
      };
      this._collators[country] = languages.map(
        lang => new Intl.Collator(lang, options)
      );
    }
    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 });
    }, []);
  },

  /**
   * Used to populate dropdowns in the UI (e.g. FormAutofill preferences, Web Payments).
   * Use findAddressSelectOption for matching a value to a region.
   *
   * @param {string[]} subKeys An array of regionCode strings
   * @param {string[]} subIsoids An array of ISO ID strings, if provided will be preferred over the key
   * @param {string[]} subNames An array of regionName strings
   * @param {string[]} subLnames An array of latinised regionName strings
   * @returns {Map?} Returns null if subKeys or subNames are not truthy.
   *                   Otherwise, a Map will be returned mapping keys -> names.
   */
  buildRegionMapIfAvailable(subKeys, subIsoids, subNames, subLnames) {
    // Not all regions have sub_keys. e.g. DE
    if (
      !subKeys ||
      !subKeys.length ||
      (!subNames && !subLnames) ||
      ((subNames && subKeys.length != subNames.length) ||
        (subLnames && subKeys.length != subLnames.length))
    ) {
      return null;
    }

    // Overwrite subKeys with subIsoids, when available
    if (subIsoids && subIsoids.length && subIsoids.length == subKeys.length) {
      for (let i = 0; i < subIsoids.length; i++) {
        if (subIsoids[i]) {
          subKeys[i] = subIsoids[i];
        }
      }
    }

    // Apply sub_lnames if sub_names does not exist
    let names = subNames || subLnames;
    return new Map(subKeys.map((key, index) => [key, names[index]]));
  },

  /**
   * 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.getSearchCollators(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.getSearchCollators(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.getSearchCollators(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) {
    let dataset = this.getCountryAddressData(country);
    // We hit a country fallback in `getCountryAddressRawData` but it's not relevant here.
    if (country != dataset.key) {
      // Use a sparse object so the below default values take effect.
      dataset = {
        /**
         * Even though data/ZZ only has address-level2, include the other levels
         * in case they are needed for unknown countries. Users can leave the
         * unnecessary fields blank which is better than forcing users to enter
         * the data in incorrect fields.
         */
        fmt: "%N%n%O%n%A%n%C %S %Z",
      };
    }
    return {
      // When particular values are missing for a country, the
      // data/ZZ value should be used instead:
      // https://chromium-i18n.appspot.com/ssl-aggregate-address/data/ZZ
      addressLevel3Label: dataset.sublocality_name_type || "suburb",
      addressLevel2Label: dataset.locality_name_type || "city",
      addressLevel1Label: dataset.state_name_type || "province",
      addressLevel1Options: this.buildRegionMapIfAvailable(
        dataset.sub_keys,
        dataset.sub_isoids,
        dataset.sub_names,
        dataset.sub_lnames
      ),
      countryRequiredFields: this.parseRequireString(dataset.require || "AC"),
      fieldsOrder: this.parseAddressFormat(dataset.fmt || "%N%n%O%n%A%n%C"),
      postalCodeLabel: dataset.zip_name_type || "postalCode",
      postalCodePattern: dataset.zip,
    };
  },

  /**
   * 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;
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
!<²;‡Â&Â&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"];

const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { XPCOMUtils } = 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
        );
      }
    });

    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
    );

    // 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
!<_ó8C2C2(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"];

const { XPCOMUtils } = ChromeUtils.import(
  "resource://gre/modules/XPCOMUtils.jsm"
);
const { FormAutofill } = 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]
        ),
      ];
    }

    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
!<«¹ÁҲ¢²¢/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
!<%ˆ)GCC2chrome/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
!<¼„Ÿÿ••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;
  opacity: .5;
  padding-inline-start: 1em;
}

:root:not([subdialog]) #country-warning-message {
  display: none;
}

/* Reset margins for inputs and textareas, overriding in-content styles */
#form textarea,
#form input {
  margin: 0;
}
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
!<¢¸Œš	š	 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;
  opacity: .5;
  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);
  opacity: 1;
}

/* 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
!<&oŒã>ã>+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
!<Èéj‹íí5chrome/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
!</Ÿøïï*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 and Autofill
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
!<µ)«..chrome.manifestPK
!<H>÷ߤ¤¤[api.jsPK
!<¿C›……
¤#background.jsPK
!<Ëû¯*)¤Óchrome/content/FormAutofillFrameScript.jsPK
!<üBõNõN#¤-chrome/content/autofillEditForms.jsPK
!<ŸXª*¼0¼0 ¤T|chrome/content/customElements.jsPK
!<r<ÓÆÆ ¤N­chrome/content/editAddress.xhtmlPK
!<- É*€€#¤RÀchrome/content/editCreditCard.xhtmlPK
!<no§@@¤Ñchrome/content/editDialog.jsPK
!<*1--¤ächrome/content/formautofill.cssPK
!<¡iet™™"¤÷ìchrome/content/formfill-anchor.svgPK
!<ˆ@_{µ!µ!"¤Ððchrome/content/heuristicsRegexp.jsPK
!<?Ó<Ñmm$¤Åchrome/content/icon-address-save.svgPK
!<0³Zqjj&¤tchrome/content/icon-address-update.svgPK
!<QÒù!¶¶+¤"chrome/content/icon-credit-card-generic.svgPK
!<ó§rýý#¤!chrome/content/icon-credit-card.svgPK
!<éF2	qq¤_ chrome/content/l10n.jsPK
!<`4A@@$¤(chrome/content/manageAddresses.xhtmlPK
!<f+UDNN&¤†.chrome/content/manageCreditCards.xhtmlPK
!<ŠG$æ¤5chrome/content/manageDialog.cssPK
!<`ehYÒ1Ò1¤lAchrome/content/manageDialog.jsPK
!<ï(O˜+¤zschrome/content/third-party/cc-logo-amex.pngPK
!<°Ba		.¤Ýxchrome/content/third-party/cc-logo-amex@2x.pngPK
!<+©82ØØ4¤0‚chrome/content/third-party/cc-logo-cartebancaire.pngPK
!<ۺ¯''7¤Z‡chrome/content/third-party/cc-logo-cartebancaire@2x.pngPK
!<øu+-¤֓chrome/content/third-party/cc-logo-diners.svgPK
!<Ȋ+]]/¤>¤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“ÎÎ*¤dÒchrome/content/third-party/cc-logo-mir.svgPK
!<ÀzÄ"ÆÆ/¤zÖchrome/content/third-party/cc-logo-unionpay.svgPK
!<ñö '""+¤ôchrome/content/third-party/cc-logo-visa.svgPK
!<;ÕËøùchrome/res/FormAutofill.jsmPK
!<Ÿϖ!9\9\"¤ß	chrome/res/FormAutofillContent.jsmPK
!<%ަ9=9=%¤Xfchrome/res/FormAutofillDoorhanger.jsmPK
!<™•".Œ.Œ"¤ԣchrome/res/FormAutofillHandler.jsmPK
!<˜r¥­””%¤B0chrome/res/FormAutofillHeuristics.jsmPK
!<›Sl*l*$¤Ächrome/res/FormAutofillNameUtils.jsmPK
!<ºR/|b|b!¤;ïchrome/res/FormAutofillParent.jsmPK
!<NHþ;p(p(&¤öQchrome/res/FormAutofillPreferences.jsmPK
!<Qzd‘¬¬"¤ªzchrome/res/FormAutofillStorage.jsmPK
!<}f1ö®-®-¤–ƒchrome/res/FormAutofillSync.jsmPK
!<µ}B†Š†Š ¤±chrome/res/FormAutofillUtils.jsmPK
!<²;‡Â&Â&¤E<chrome/res/OSKeyStore.jsmPK
!<_ó8C2C2(¤>cchrome/res/ProfileAutoCompleteResult.jsmPK
!<«¹ÁҲ¢²¢/¤Ǖchrome/res/addressmetadata/addressReferences.jsPK
!<%ˆ)GCC2¤Æ8chrome/res/addressmetadata/addressReferencesExt.jsPK
!<ܬ«º™™'¤Y<chrome/res/autocomplete-item-shared.cssPK
!<ûg“55 ¤7Ochrome/res/autocomplete-item.cssPK
!<¼„Ÿÿ••¤ªPchrome/res/editAddress.cssPK
!<qû²ÅŤw`chrome/res/editCreditCard.cssPK
!<¢¸Œš	š	 ¤wechrome/res/editDialog-shared.cssPK
!<7D⤁Oochrome/res/editDialog.cssPK
!<&oŒã>ã>+¤Ÿpchrome/res/phonenumberutils/PhoneNumber.jsmPK
!<ôg`RÚÚ3¤˯chrome/res/phonenumberutils/PhoneNumberMetaData.jsmPK
!<Èéj‹íí5¤ö¶	chrome/res/phonenumberutils/PhoneNumberNormalizer.jsmPK
!</Ÿøïï*6¾	en-US/locale/en-US/formautofill.propertiesPK
!<¦ß~‹ªª
¤mÐ	manifest.jsonPK
!<DÒhp¤BÒ	schema.jsonPK<<!nÒ