Bug 1941046 - Part 4: Send a callback request for impression and clicks of MARS Top...
[gecko.git] / toolkit / components / formautofill / shared / PhoneNumber.sys.mjs
blob5288765181ab4ca7393aa373fe85a64f3ec5f84a
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";
10 const lazy = {};
12 ChromeUtils.defineESModuleGetters(lazy, {
13   PhoneNumberNormalizer:
14     "resource://gre/modules/shared/PhoneNumberNormalizer.sys.mjs",
15 });
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 = [
29     "region",
30     "^(?:internationalPrefix)",
31     "nationalPrefix",
32     "^(?:nationalPrefixForParsing)",
33     "nationalPrefixTransformRule",
34     "nationalPrefixFormattingRule",
35     "^possiblePattern$",
36     "^nationalPattern$",
37     "formats",
38   ];
40   const FORMAT_ENCODING = [
41     "^pattern$",
42     "nationalFormat",
43     "^leadingDigits",
44     "nationalPrefixFormattingRule",
45     "internationalFormat",
46   ];
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) {
54       let value = array[n];
55       if (!value) {
56         continue;
57       }
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));
62       }
63       obj[fieldAlpha] = value;
64     }
65     return obj;
66   }
68   // Parse string encoded meta data into a convenient object
69   // representation.
70   function ParseMetaData(countryCode, md) {
71     let array = JSON.parse(md);
72     md = ParseArray(array, META_DATA_ENCODING, { countryCode });
73     regionCache[md.region] = md;
74     return md;
75   }
77   // Parse string encoded format data into a convenient object
78   // representation.
79   function ParseFormat(md) {
80     let formats = md.formats;
81     if (!formats) {
82       return;
83     }
84     // Bail if we already parsed the format definitions.
85     if (!Array.isArray(formats[0])) {
86       return;
87     }
88     for (let n = 0; n < formats.length; ++n) {
89       formats[n] = ParseArray(formats[n], FORMAT_ENCODING, {});
90     }
91   }
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];
101     if (md) {
102       return md;
103     }
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) {
116             if (n > 0) {
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]);
122               }
123               let formats = entry[0].formats;
124               let current = ParseMetaData(countryCode, entry[n]);
125               current.formats = formats;
126               entry[n] = current;
127               return entry[n];
128             }
130             entry[n] = ParseMetaData(countryCode, entry[n]);
131             return entry[n];
132           }
133         }
134         continue;
135       }
136       if (typeof entry == "string" && entry.substr(2, 2) == region) {
137         dataBase[countryCode] = ParseMetaData(countryCode, entry);
138         return dataBase[countryCode];
139       }
140     }
141   }
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;
150     if (!formats) {
151       return null;
152     }
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)) {
158         continue;
159       }
160       if (!format.pattern.test(number)) {
161         continue;
162       }
163       if (intl) {
164         // If there is no international format, just fall back to the national
165         // format.
166         let internationalFormat = format.internationalFormat;
167         if (!internationalFormat) {
168           internationalFormat = format.nationalFormat;
169         }
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") {
174           return null;
175         }
176         // Prepend "+" and the country code.
177         number =
178           "+" +
179           regionMetaData.countryCode +
180           " " +
181           number.replace(format.pattern, internationalFormat);
182       } else {
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;
190         }
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);
196           if (match) {
197             let firstGroup = match[1];
198             let rest = match[2];
199             let prefix = nationalPrefixFormattingRule;
200             prefix = prefix.replace("$NP", regionMetaData.nationalPrefix);
201             prefix = prefix.replace("$FG", firstGroup);
202             number = prefix + rest;
203           }
204         }
205       }
206       return number == "NA" ? null : number;
207     }
208     return null;
209   }
211   function NationalNumber(regionMetaData, number) {
212     this.region = regionMetaData.region;
213     this.regionMetaData = regionMetaData;
214     this.number = number;
215   }
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 = {
222     // +1 949-726-2896
223     get internationalFormat() {
224       let value = FormatNumber(this.regionMetaData, this.number, true);
225       Object.defineProperty(this, "internationalFormat", {
226         value,
227         enumerable: true,
228       });
229       return value;
230     },
231     // (949) 726-2896
232     get nationalFormat() {
233       let value = FormatNumber(this.regionMetaData, this.number, false);
234       Object.defineProperty(this, "nationalFormat", {
235         value,
236         enumerable: true,
237       });
238       return value;
239     },
240     // +19497262896
241     get internationalNumber() {
242       let value = this.internationalFormat
243         ? this.internationalFormat.replace(NON_DIALABLE_CHARS, "")
244         : null;
245       Object.defineProperty(this, "internationalNumber", {
246         value,
247         enumerable: true,
248       });
249       return value;
250     },
251     // 9497262896
252     get nationalNumber() {
253       let value = this.nationalFormat
254         ? this.nationalFormat.replace(NON_DIALABLE_CHARS, "")
255         : null;
256       Object.defineProperty(this, "nationalNumber", {
257         value,
258         enumerable: true,
259       });
260       return value;
261     },
262     // country name 'US'
263     get countryName() {
264       let value = this.region ? this.region : null;
265       Object.defineProperty(this, "countryName", { value, enumerable: true });
266       return value;
267     },
268     // country code '+1'
269     get countryCode() {
270       let value = this.regionMetaData.countryCode
271         ? "+" + this.regionMetaData.countryCode
272         : null;
273       Object.defineProperty(this, "countryCode", { value, enumerable: true });
274       return value;
275     },
276   };
278   // Check whether the number is valid for the given region.
279   function IsValidNumber(number, md) {
280     return md.possiblePattern.test(number);
281   }
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);
287   }
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);
294       if (dataBase[cc]) {
295         return cc;
296       }
297     }
298     return null;
299   }
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)) {
306       return null;
307     }
308     // Success.
309     return new NationalNumber(md, number);
310   }
312   function ParseNationalNumberAndCheckNationalPrefix(number, md) {
313     let ret;
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 || ""
323       );
324       ret = ParseNationalNumber(withoutPrefix, md);
325       if (ret) {
326         return ret;
327       }
328     } else {
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;
332       if (
333         nationalPrefix &&
334         number.indexOf(nationalPrefix) == 0 &&
335         (ret = ParseNationalNumber(number.substr(nationalPrefix.length), md))
336       ) {
337         return ret;
338       }
339     }
340     ret = ParseNationalNumber(number, md);
341     if (ret) {
342       return ret;
343     }
344   }
346   function ParseNumberByCountryCode(number, countryCode) {
347     let ret;
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]);
356         }
357         if (n > 0) {
358           entry[n].formats = entry[0].formats;
359         }
360         ret = ParseNationalNumberAndCheckNationalPrefix(number, entry[n]);
361         if (ret) {
362           return ret;
363         }
364       }
365       return null;
366     }
367     if (typeof entry == "string") {
368       entry = dataBase[countryCode] = ParseMetaData(countryCode, entry);
369     }
370     return ParseNationalNumberAndCheckNationalPrefix(number, entry);
371   }
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);
378     if (!countryCode) {
379       return null;
380     }
381     number = number.substr(countryCode.length);
383     return ParseNumberByCountryCode(number, countryCode);
384   }
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) {
389     let ret;
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] !== "+") {
397       return null;
398     }
400     // Detect and strip leading '+'.
401     if (number[0] === "+") {
402       return ParseInternationalNumber(
403         number.replace(LEADING_PLUS_CHARS_PATTERN, "")
404       );
405     }
407     // If "defaultRegion" is a country code, use it to parse the number directly.
408     let matches = String(defaultRegion).match(/^\+?(\d+)/);
409     if (matches) {
410       let countryCode = ParseCountryCode(matches[1]);
411       if (!countryCode) {
412         return null;
413       }
414       return ParseNumberByCountryCode(number, countryCode);
415     }
417     // Lookup the meta data for the given region.
418     let md = FindMetaDataForRegion(defaultRegion.toUpperCase());
419     if (!md) {
420       dump("Couldn't find Meta Data for region: " + defaultRegion + "\n");
421       return null;
422     }
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);
430       if (ret) {
431         return ret;
432       }
433     }
435     ret = ParseNationalNumberAndCheckNationalPrefix(number, md);
436     if (ret) {
437       return ret;
438     }
440     // Now lets see if maybe its an international number after all, but
441     // without '+' or the international prefix.
442     ret = ParseInternationalNumber(number);
443     if (ret) {
444       return ret;
445     }
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);
451     }
453     // We couldn't parse the number at all.
454     return null;
455   }
457   function IsPlainPhoneNumber(number) {
458     if (typeof number !== "string") {
459       return false;
460     }
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));
466   }
468   return {
469     IsPlain: IsPlainPhoneNumber,
470     IsValid: IsValidNumber,
471     Parse: ParseNumber,
472     FindMetaDataForRegion,
473   };
474 })(PHONE_NUMBER_META_DATA);