1 /* This Source Code Form is subject to the terms of the Apache License, Version
2 * 2.0. If a copy of the Apache License was not distributed with this file, You
3 * can obtain one at https://www.apache.org/licenses/LICENSE-2.0 */
5 // This library came from https://github.com/andreasgal/PhoneNumber.js but will
6 // be further maintained by our own in Form Autofill codebase.
8 import { PHONE_NUMBER_META_DATA } from "resource://gre/modules/shared/PhoneNumberMetaData.sys.mjs";
12 ChromeUtils.defineESModuleGetters(lazy, {
13 PhoneNumberNormalizer:
14 "resource://gre/modules/shared/PhoneNumberNormalizer.sys.mjs",
17 export var PhoneNumber = (function (dataBase) {
18 const MAX_PHONE_NUMBER_LENGTH = 50;
19 const NON_ALPHA_CHARS = /[^a-zA-Z]/g;
20 const NON_DIALABLE_CHARS = /[^,#+\*\d]/g;
21 const NON_DIALABLE_CHARS_ONCE = new RegExp(NON_DIALABLE_CHARS.source);
22 const SPLIT_FIRST_GROUP = /^(\d+)(.*)$/;
23 const LEADING_PLUS_CHARS_PATTERN = /^[+\uFF0B]+/g;
25 // Format of the string encoded meta data. If the name contains "^" or "$"
26 // we will generate a regular expression from the value, with those special
27 // characters as prefix/suffix.
28 const META_DATA_ENCODING = [
30 "^(?:internationalPrefix)",
32 "^(?:nationalPrefixForParsing)",
33 "nationalPrefixTransformRule",
34 "nationalPrefixFormattingRule",
40 const FORMAT_ENCODING = [
44 "nationalPrefixFormattingRule",
45 "internationalFormat",
48 let regionCache = Object.create(null);
50 // Parse an array of strings into a convenient object. We store meta
51 // data as arrays since thats much more compact than JSON.
52 function ParseArray(array, encoding, obj) {
53 for (let n = 0; n < encoding.length; ++n) {
58 let field = encoding[n];
59 let fieldAlpha = field.replace(NON_ALPHA_CHARS, "");
60 if (field != fieldAlpha) {
61 value = new RegExp(field.replace(fieldAlpha, value));
63 obj[fieldAlpha] = value;
68 // Parse string encoded meta data into a convenient object
70 function ParseMetaData(countryCode, md) {
71 let array = JSON.parse(md);
72 md = ParseArray(array, META_DATA_ENCODING, { countryCode });
73 regionCache[md.region] = md;
77 // Parse string encoded format data into a convenient object
79 function ParseFormat(md) {
80 let formats = md.formats;
84 // Bail if we already parsed the format definitions.
85 if (!Array.isArray(formats[0])) {
88 for (let n = 0; n < formats.length; ++n) {
89 formats[n] = ParseArray(formats[n], FORMAT_ENCODING, {});
93 // Search for the meta data associated with a region identifier ("US") in
94 // our database, which is indexed by country code ("1"). Since we have
95 // to walk the entire database for this, we cache the result of the lookup
96 // for future reference.
97 function FindMetaDataForRegion(region) {
98 // Check in the region cache first. This will find all entries we have
99 // already resolved (parsed from a string encoding).
100 let md = regionCache[region];
104 for (let countryCode in dataBase) {
105 let entry = dataBase[countryCode];
106 // Each entry is a string encoded object of the form '["US..', or
107 // an array of strings. We don't want to parse the string here
108 // to save memory, so we just substring the region identifier
109 // and compare it. For arrays, we compare against all region
110 // identifiers with that country code. We skip entries that are
111 // of type object, because they were already resolved (parsed into
112 // an object), and their country code should have been in the cache.
113 if (Array.isArray(entry)) {
114 for (let n = 0; n < entry.length; n++) {
115 if (typeof entry[n] == "string" && entry[n].substr(2, 2) == region) {
117 // Only the first entry has the formats field set.
118 // Parse the main country if we haven't already and use
119 // the formats field from the main country.
120 if (typeof entry[0] == "string") {
121 entry[0] = ParseMetaData(countryCode, entry[0]);
123 let formats = entry[0].formats;
124 let current = ParseMetaData(countryCode, entry[n]);
125 current.formats = formats;
130 entry[n] = ParseMetaData(countryCode, entry[n]);
136 if (typeof entry == "string" && entry.substr(2, 2) == region) {
137 dataBase[countryCode] = ParseMetaData(countryCode, entry);
138 return dataBase[countryCode];
143 // Format a national number for a given region. The boolean flag "intl"
144 // indicates whether we want the national or international format.
145 function FormatNumber(regionMetaData, number, intl) {
146 // We lazily parse the format description in the meta data for the region,
147 // so make sure to parse it now if we haven't already done so.
148 ParseFormat(regionMetaData);
149 let formats = regionMetaData.formats;
153 for (let n = 0; n < formats.length; ++n) {
154 let format = formats[n];
155 // The leading digits field is optional. If we don't have it, just
156 // use the matching pattern to qualify numbers.
157 if (format.leadingDigits && !format.leadingDigits.test(number)) {
160 if (!format.pattern.test(number)) {
164 // If there is no international format, just fall back to the national
166 let internationalFormat = format.internationalFormat;
167 if (!internationalFormat) {
168 internationalFormat = format.nationalFormat;
170 // Some regions have numbers that can't be dialed from outside the
171 // country, indicated by "NA" for the international format of that
172 // number format pattern.
173 if (internationalFormat == "NA") {
176 // Prepend "+" and the country code.
179 regionMetaData.countryCode +
181 number.replace(format.pattern, internationalFormat);
183 number = number.replace(format.pattern, format.nationalFormat);
184 // The region has a national prefix formatting rule, and it can be overwritten
185 // by each actual number format rule.
186 let nationalPrefixFormattingRule =
187 regionMetaData.nationalPrefixFormattingRule;
188 if (format.nationalPrefixFormattingRule) {
189 nationalPrefixFormattingRule = format.nationalPrefixFormattingRule;
191 if (nationalPrefixFormattingRule) {
192 // The prefix formatting rule contains two magic markers, "$NP" and "$FG".
193 // "$NP" will be replaced by the national prefix, and "$FG" with the
194 // first group of numbers.
195 let match = number.match(SPLIT_FIRST_GROUP);
197 let firstGroup = match[1];
199 let prefix = nationalPrefixFormattingRule;
200 prefix = prefix.replace("$NP", regionMetaData.nationalPrefix);
201 prefix = prefix.replace("$FG", firstGroup);
202 number = prefix + rest;
206 return number == "NA" ? null : number;
211 function NationalNumber(regionMetaData, number) {
212 this.region = regionMetaData.region;
213 this.regionMetaData = regionMetaData;
214 this.number = number;
217 // NationalNumber represents the result of parsing a phone number. We have
218 // three getters on the prototype that format the number in national and
219 // international format. Once called, the getters put a direct property
220 // onto the object, caching the result.
221 NationalNumber.prototype = {
223 get internationalFormat() {
224 let value = FormatNumber(this.regionMetaData, this.number, true);
225 Object.defineProperty(this, "internationalFormat", {
232 get nationalFormat() {
233 let value = FormatNumber(this.regionMetaData, this.number, false);
234 Object.defineProperty(this, "nationalFormat", {
241 get internationalNumber() {
242 let value = this.internationalFormat
243 ? this.internationalFormat.replace(NON_DIALABLE_CHARS, "")
245 Object.defineProperty(this, "internationalNumber", {
252 get nationalNumber() {
253 let value = this.nationalFormat
254 ? this.nationalFormat.replace(NON_DIALABLE_CHARS, "")
256 Object.defineProperty(this, "nationalNumber", {
264 let value = this.region ? this.region : null;
265 Object.defineProperty(this, "countryName", { value, enumerable: true });
270 let value = this.regionMetaData.countryCode
271 ? "+" + this.regionMetaData.countryCode
273 Object.defineProperty(this, "countryCode", { value, enumerable: true });
278 // Check whether the number is valid for the given region.
279 function IsValidNumber(number, md) {
280 return md.possiblePattern.test(number);
283 // Check whether the number is a valid national number for the given region.
284 /* eslint-disable no-unused-vars */
285 function IsNationalNumber(number, md) {
286 return IsValidNumber(number, md) && md.nationalPattern.test(number);
289 // Determine the country code a number starts with, or return null if
290 // its not a valid country code.
291 function ParseCountryCode(number) {
292 for (let n = 1; n <= 3; ++n) {
293 let cc = number.substr(0, n);
301 // Parse a national number for a specific region. Return null if the
302 // number is not a valid national number (it might still be a possible
303 // number for parts of that region).
304 function ParseNationalNumber(number, md) {
305 if (!md.possiblePattern.test(number) || !md.nationalPattern.test(number)) {
309 return new NationalNumber(md, number);
312 function ParseNationalNumberAndCheckNationalPrefix(number, md) {
315 // This is not an international number. See if its a national one for
316 // the current region. National numbers can start with the national
317 // prefix, or without.
318 if (md.nationalPrefixForParsing) {
319 // Some regions have specific national prefix parse rules. Apply those.
320 let withoutPrefix = number.replace(
321 md.nationalPrefixForParsing,
322 md.nationalPrefixTransformRule || ""
324 ret = ParseNationalNumber(withoutPrefix, md);
329 // If there is no specific national prefix rule, just strip off the
330 // national prefix from the beginning of the number (if there is one).
331 let nationalPrefix = md.nationalPrefix;
334 number.indexOf(nationalPrefix) == 0 &&
335 (ret = ParseNationalNumber(number.substr(nationalPrefix.length), md))
340 ret = ParseNationalNumber(number, md);
346 function ParseNumberByCountryCode(number, countryCode) {
349 // Lookup the meta data for the region (or regions) and if the rest of
350 // the number parses for that region, return the parsed number.
351 let entry = dataBase[countryCode];
352 if (Array.isArray(entry)) {
353 for (let n = 0; n < entry.length; ++n) {
354 if (typeof entry[n] == "string") {
355 entry[n] = ParseMetaData(countryCode, entry[n]);
358 entry[n].formats = entry[0].formats;
360 ret = ParseNationalNumberAndCheckNationalPrefix(number, entry[n]);
367 if (typeof entry == "string") {
368 entry = dataBase[countryCode] = ParseMetaData(countryCode, entry);
370 return ParseNationalNumberAndCheckNationalPrefix(number, entry);
373 // Parse an international number that starts with the country code. Return
374 // null if the number is not a valid international number.
375 function ParseInternationalNumber(number) {
376 // Parse and strip the country code.
377 let countryCode = ParseCountryCode(number);
381 number = number.substr(countryCode.length);
383 return ParseNumberByCountryCode(number, countryCode);
386 // Parse a number and transform it into the national format, removing any
387 // international dial prefixes and country codes.
388 function ParseNumber(number, defaultRegion) {
391 // Remove formating characters and whitespace.
392 number = lazy.PhoneNumberNormalizer.Normalize(number);
394 // If there is no defaultRegion or the defaultRegion is the global region,
395 // we can't parse international access codes.
396 if ((!defaultRegion || defaultRegion === "001") && number[0] !== "+") {
400 // Detect and strip leading '+'.
401 if (number[0] === "+") {
402 return ParseInternationalNumber(
403 number.replace(LEADING_PLUS_CHARS_PATTERN, "")
407 // If "defaultRegion" is a country code, use it to parse the number directly.
408 let matches = String(defaultRegion).match(/^\+?(\d+)/);
410 let countryCode = ParseCountryCode(matches[1]);
414 return ParseNumberByCountryCode(number, countryCode);
417 // Lookup the meta data for the given region.
418 let md = FindMetaDataForRegion(defaultRegion.toUpperCase());
420 dump("Couldn't find Meta Data for region: " + defaultRegion + "\n");
424 // See if the number starts with an international prefix, and if the
425 // number resulting from stripping the code is valid, then remove the
426 // prefix and flag the number as international.
427 if (md.internationalPrefix.test(number)) {
428 let possibleNumber = number.replace(md.internationalPrefix, "");
429 ret = ParseInternationalNumber(possibleNumber);
435 ret = ParseNationalNumberAndCheckNationalPrefix(number, md);
440 // Now lets see if maybe its an international number after all, but
441 // without '+' or the international prefix.
442 ret = ParseInternationalNumber(number);
447 // If the number matches the possible numbers of the current region,
448 // return it as a possible number.
449 if (md.possiblePattern.test(number)) {
450 return new NationalNumber(md, number);
453 // We couldn't parse the number at all.
457 function IsPlainPhoneNumber(number) {
458 if (typeof number !== "string") {
462 let length = number.length;
463 let isTooLong = length > MAX_PHONE_NUMBER_LENGTH;
464 let isEmpty = length === 0;
465 return !(isTooLong || isEmpty || NON_DIALABLE_CHARS_ONCE.test(number));
469 IsPlain: IsPlainPhoneNumber,
470 IsValid: IsValidNumber,
472 FindMetaDataForRegion,
474 })(PHONE_NUMBER_META_DATA);