summaryrefslogtreecommitdiffstats
path: root/toolkit/components/formautofill/phonenumberutils/PhoneNumber.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/formautofill/phonenumberutils/PhoneNumber.jsm')
-rw-r--r--toolkit/components/formautofill/phonenumberutils/PhoneNumber.jsm483
1 files changed, 483 insertions, 0 deletions
diff --git a/toolkit/components/formautofill/phonenumberutils/PhoneNumber.jsm b/toolkit/components/formautofill/phonenumberutils/PhoneNumber.jsm
new file mode 100644
index 0000000000..b773e35f77
--- /dev/null
+++ b/toolkit/components/formautofill/phonenumberutils/PhoneNumber.jsm
@@ -0,0 +1,483 @@
+/* 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"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const { PHONE_NUMBER_META_DATA } = ChromeUtils.import(
+ "resource://autofill/phonenumberutils/PhoneNumberMetaData.jsm"
+);
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ PhoneNumberNormalizer:
+ "resource://autofill/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 = lazy.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);