Sat, 03 Jan 2015 20:18:00 +0100
Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.
michael@0 | 1 | /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
michael@0 | 2 | /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ |
michael@0 | 3 | |
michael@0 | 4 | // Don't modify this code. Please use: |
michael@0 | 5 | // https://github.com/andreasgal/PhoneNumber.js |
michael@0 | 6 | |
michael@0 | 7 | "use strict"; |
michael@0 | 8 | |
michael@0 | 9 | this.EXPORTED_SYMBOLS = ["PhoneNumber"]; |
michael@0 | 10 | |
michael@0 | 11 | Components.utils.import("resource://gre/modules/PhoneNumberMetaData.jsm"); |
michael@0 | 12 | Components.utils.import("resource://gre/modules/PhoneNumberNormalizer.jsm"); |
michael@0 | 13 | |
michael@0 | 14 | this.PhoneNumber = (function (dataBase) { |
michael@0 | 15 | // Use strict in our context only - users might not want it |
michael@0 | 16 | 'use strict'; |
michael@0 | 17 | |
michael@0 | 18 | const MAX_PHONE_NUMBER_LENGTH = 50; |
michael@0 | 19 | const NON_ALPHA_CHARS = /[^a-zA-Z]/g; |
michael@0 | 20 | const NON_DIALABLE_CHARS = /[^,#+\*\d]/g; |
michael@0 | 21 | const NON_DIALABLE_CHARS_ONCE = new RegExp(NON_DIALABLE_CHARS.source); |
michael@0 | 22 | const BACKSLASH = /\\/g; |
michael@0 | 23 | const SPLIT_FIRST_GROUP = /^(\d+)(.*)$/; |
michael@0 | 24 | const LEADING_PLUS_CHARS_PATTERN = /^[+\uFF0B]+/g; |
michael@0 | 25 | |
michael@0 | 26 | // Format of the string encoded meta data. If the name contains "^" or "$" |
michael@0 | 27 | // we will generate a regular expression from the value, with those special |
michael@0 | 28 | // characters as prefix/suffix. |
michael@0 | 29 | const META_DATA_ENCODING = ["region", |
michael@0 | 30 | "^(?:internationalPrefix)", |
michael@0 | 31 | "nationalPrefix", |
michael@0 | 32 | "^(?:nationalPrefixForParsing)", |
michael@0 | 33 | "nationalPrefixTransformRule", |
michael@0 | 34 | "nationalPrefixFormattingRule", |
michael@0 | 35 | "^possiblePattern$", |
michael@0 | 36 | "^nationalPattern$", |
michael@0 | 37 | "formats"]; |
michael@0 | 38 | |
michael@0 | 39 | const FORMAT_ENCODING = ["^pattern$", |
michael@0 | 40 | "nationalFormat", |
michael@0 | 41 | "^leadingDigits", |
michael@0 | 42 | "nationalPrefixFormattingRule", |
michael@0 | 43 | "internationalFormat"]; |
michael@0 | 44 | |
michael@0 | 45 | var regionCache = Object.create(null); |
michael@0 | 46 | |
michael@0 | 47 | // Parse an array of strings into a convenient object. We store meta |
michael@0 | 48 | // data as arrays since thats much more compact than JSON. |
michael@0 | 49 | function ParseArray(array, encoding, obj) { |
michael@0 | 50 | for (var n = 0; n < encoding.length; ++n) { |
michael@0 | 51 | var value = array[n]; |
michael@0 | 52 | if (!value) |
michael@0 | 53 | continue; |
michael@0 | 54 | var field = encoding[n]; |
michael@0 | 55 | var fieldAlpha = field.replace(NON_ALPHA_CHARS, ""); |
michael@0 | 56 | if (field != fieldAlpha) |
michael@0 | 57 | value = new RegExp(field.replace(fieldAlpha, value)); |
michael@0 | 58 | obj[fieldAlpha] = value; |
michael@0 | 59 | } |
michael@0 | 60 | return obj; |
michael@0 | 61 | } |
michael@0 | 62 | |
michael@0 | 63 | // Parse string encoded meta data into a convenient object |
michael@0 | 64 | // representation. |
michael@0 | 65 | function ParseMetaData(countryCode, md) { |
michael@0 | 66 | var array = eval(md.replace(BACKSLASH, "\\\\")); |
michael@0 | 67 | md = ParseArray(array, |
michael@0 | 68 | META_DATA_ENCODING, |
michael@0 | 69 | { countryCode: countryCode }); |
michael@0 | 70 | regionCache[md.region] = md; |
michael@0 | 71 | return md; |
michael@0 | 72 | } |
michael@0 | 73 | |
michael@0 | 74 | // Parse string encoded format data into a convenient object |
michael@0 | 75 | // representation. |
michael@0 | 76 | function ParseFormat(md) { |
michael@0 | 77 | var formats = md.formats; |
michael@0 | 78 | if (!formats) { |
michael@0 | 79 | return null; |
michael@0 | 80 | } |
michael@0 | 81 | // Bail if we already parsed the format definitions. |
michael@0 | 82 | if (!(Array.isArray(formats[0]))) |
michael@0 | 83 | return; |
michael@0 | 84 | for (var n = 0; n < formats.length; ++n) { |
michael@0 | 85 | formats[n] = ParseArray(formats[n], |
michael@0 | 86 | FORMAT_ENCODING, |
michael@0 | 87 | {}); |
michael@0 | 88 | } |
michael@0 | 89 | } |
michael@0 | 90 | |
michael@0 | 91 | // Search for the meta data associated with a region identifier ("US") in |
michael@0 | 92 | // our database, which is indexed by country code ("1"). Since we have |
michael@0 | 93 | // to walk the entire database for this, we cache the result of the lookup |
michael@0 | 94 | // for future reference. |
michael@0 | 95 | function FindMetaDataForRegion(region) { |
michael@0 | 96 | // Check in the region cache first. This will find all entries we have |
michael@0 | 97 | // already resolved (parsed from a string encoding). |
michael@0 | 98 | var md = regionCache[region]; |
michael@0 | 99 | if (md) |
michael@0 | 100 | return md; |
michael@0 | 101 | for (var countryCode in dataBase) { |
michael@0 | 102 | var entry = dataBase[countryCode]; |
michael@0 | 103 | // Each entry is a string encoded object of the form '["US..', or |
michael@0 | 104 | // an array of strings. We don't want to parse the string here |
michael@0 | 105 | // to save memory, so we just substring the region identifier |
michael@0 | 106 | // and compare it. For arrays, we compare against all region |
michael@0 | 107 | // identifiers with that country code. We skip entries that are |
michael@0 | 108 | // of type object, because they were already resolved (parsed into |
michael@0 | 109 | // an object), and their country code should have been in the cache. |
michael@0 | 110 | if (Array.isArray(entry)) { |
michael@0 | 111 | for (var n = 0; n < entry.length; n++) { |
michael@0 | 112 | if (typeof entry[n] == "string" && entry[n].substr(2,2) == region) { |
michael@0 | 113 | if (n > 0) { |
michael@0 | 114 | // Only the first entry has the formats field set. |
michael@0 | 115 | // Parse the main country if we haven't already and use |
michael@0 | 116 | // the formats field from the main country. |
michael@0 | 117 | if (typeof entry[0] == "string") |
michael@0 | 118 | entry[0] = ParseMetaData(countryCode, entry[0]); |
michael@0 | 119 | let formats = entry[0].formats; |
michael@0 | 120 | let current = ParseMetaData(countryCode, entry[n]); |
michael@0 | 121 | current.formats = formats; |
michael@0 | 122 | return entry[n] = current; |
michael@0 | 123 | } |
michael@0 | 124 | |
michael@0 | 125 | entry[n] = ParseMetaData(countryCode, entry[n]); |
michael@0 | 126 | return entry[n]; |
michael@0 | 127 | } |
michael@0 | 128 | } |
michael@0 | 129 | continue; |
michael@0 | 130 | } |
michael@0 | 131 | if (typeof entry == "string" && entry.substr(2,2) == region) |
michael@0 | 132 | return dataBase[countryCode] = ParseMetaData(countryCode, entry); |
michael@0 | 133 | } |
michael@0 | 134 | } |
michael@0 | 135 | |
michael@0 | 136 | // Format a national number for a given region. The boolean flag "intl" |
michael@0 | 137 | // indicates whether we want the national or international format. |
michael@0 | 138 | function FormatNumber(regionMetaData, number, intl) { |
michael@0 | 139 | // We lazily parse the format description in the meta data for the region, |
michael@0 | 140 | // so make sure to parse it now if we haven't already done so. |
michael@0 | 141 | ParseFormat(regionMetaData); |
michael@0 | 142 | var formats = regionMetaData.formats; |
michael@0 | 143 | if (!formats) { |
michael@0 | 144 | return null; |
michael@0 | 145 | } |
michael@0 | 146 | for (var n = 0; n < formats.length; ++n) { |
michael@0 | 147 | var format = formats[n]; |
michael@0 | 148 | // The leading digits field is optional. If we don't have it, just |
michael@0 | 149 | // use the matching pattern to qualify numbers. |
michael@0 | 150 | if (format.leadingDigits && !format.leadingDigits.test(number)) |
michael@0 | 151 | continue; |
michael@0 | 152 | if (!format.pattern.test(number)) |
michael@0 | 153 | continue; |
michael@0 | 154 | if (intl) { |
michael@0 | 155 | // If there is no international format, just fall back to the national |
michael@0 | 156 | // format. |
michael@0 | 157 | var internationalFormat = format.internationalFormat; |
michael@0 | 158 | if (!internationalFormat) |
michael@0 | 159 | internationalFormat = format.nationalFormat; |
michael@0 | 160 | // Some regions have numbers that can't be dialed from outside the |
michael@0 | 161 | // country, indicated by "NA" for the international format of that |
michael@0 | 162 | // number format pattern. |
michael@0 | 163 | if (internationalFormat == "NA") |
michael@0 | 164 | return null; |
michael@0 | 165 | // Prepend "+" and the country code. |
michael@0 | 166 | number = "+" + regionMetaData.countryCode + " " + |
michael@0 | 167 | number.replace(format.pattern, internationalFormat); |
michael@0 | 168 | } else { |
michael@0 | 169 | number = number.replace(format.pattern, format.nationalFormat); |
michael@0 | 170 | // The region has a national prefix formatting rule, and it can be overwritten |
michael@0 | 171 | // by each actual number format rule. |
michael@0 | 172 | var nationalPrefixFormattingRule = regionMetaData.nationalPrefixFormattingRule; |
michael@0 | 173 | if (format.nationalPrefixFormattingRule) |
michael@0 | 174 | nationalPrefixFormattingRule = format.nationalPrefixFormattingRule; |
michael@0 | 175 | if (nationalPrefixFormattingRule) { |
michael@0 | 176 | // The prefix formatting rule contains two magic markers, "$NP" and "$FG". |
michael@0 | 177 | // "$NP" will be replaced by the national prefix, and "$FG" with the |
michael@0 | 178 | // first group of numbers. |
michael@0 | 179 | var match = number.match(SPLIT_FIRST_GROUP); |
michael@0 | 180 | if (match) { |
michael@0 | 181 | var firstGroup = match[1]; |
michael@0 | 182 | var rest = match[2]; |
michael@0 | 183 | var prefix = nationalPrefixFormattingRule; |
michael@0 | 184 | prefix = prefix.replace("$NP", regionMetaData.nationalPrefix); |
michael@0 | 185 | prefix = prefix.replace("$FG", firstGroup); |
michael@0 | 186 | number = prefix + rest; |
michael@0 | 187 | } |
michael@0 | 188 | } |
michael@0 | 189 | } |
michael@0 | 190 | return (number == "NA") ? null : number; |
michael@0 | 191 | } |
michael@0 | 192 | return null; |
michael@0 | 193 | } |
michael@0 | 194 | |
michael@0 | 195 | function NationalNumber(regionMetaData, number) { |
michael@0 | 196 | this.region = regionMetaData.region; |
michael@0 | 197 | this.regionMetaData = regionMetaData; |
michael@0 | 198 | this.nationalNumber = number; |
michael@0 | 199 | } |
michael@0 | 200 | |
michael@0 | 201 | // NationalNumber represents the result of parsing a phone number. We have |
michael@0 | 202 | // three getters on the prototype that format the number in national and |
michael@0 | 203 | // international format. Once called, the getters put a direct property |
michael@0 | 204 | // onto the object, caching the result. |
michael@0 | 205 | NationalNumber.prototype = { |
michael@0 | 206 | // +1 949-726-2896 |
michael@0 | 207 | get internationalFormat() { |
michael@0 | 208 | var value = FormatNumber(this.regionMetaData, this.nationalNumber, true); |
michael@0 | 209 | Object.defineProperty(this, "internationalFormat", { value: value, enumerable: true }); |
michael@0 | 210 | return value; |
michael@0 | 211 | }, |
michael@0 | 212 | // (949) 726-2896 |
michael@0 | 213 | get nationalFormat() { |
michael@0 | 214 | var value = FormatNumber(this.regionMetaData, this.nationalNumber, false); |
michael@0 | 215 | Object.defineProperty(this, "nationalFormat", { value: value, enumerable: true }); |
michael@0 | 216 | return value; |
michael@0 | 217 | }, |
michael@0 | 218 | // +19497262896 |
michael@0 | 219 | get internationalNumber() { |
michael@0 | 220 | var value = this.internationalFormat ? this.internationalFormat.replace(NON_DIALABLE_CHARS, "") |
michael@0 | 221 | : null; |
michael@0 | 222 | Object.defineProperty(this, "internationalNumber", { value: value, enumerable: true }); |
michael@0 | 223 | return value; |
michael@0 | 224 | }, |
michael@0 | 225 | // country name 'US' |
michael@0 | 226 | get countryName() { |
michael@0 | 227 | var value = this.region ? this.region : null; |
michael@0 | 228 | Object.defineProperty(this, "countryName", { value: value, enumerable: true }); |
michael@0 | 229 | return value; |
michael@0 | 230 | } |
michael@0 | 231 | }; |
michael@0 | 232 | |
michael@0 | 233 | // Check whether the number is valid for the given region. |
michael@0 | 234 | function IsValidNumber(number, md) { |
michael@0 | 235 | return md.possiblePattern.test(number); |
michael@0 | 236 | } |
michael@0 | 237 | |
michael@0 | 238 | // Check whether the number is a valid national number for the given region. |
michael@0 | 239 | function IsNationalNumber(number, md) { |
michael@0 | 240 | return IsValidNumber(number, md) && md.nationalPattern.test(number); |
michael@0 | 241 | } |
michael@0 | 242 | |
michael@0 | 243 | // Determine the country code a number starts with, or return null if |
michael@0 | 244 | // its not a valid country code. |
michael@0 | 245 | function ParseCountryCode(number) { |
michael@0 | 246 | for (var n = 1; n <= 3; ++n) { |
michael@0 | 247 | var cc = number.substr(0,n); |
michael@0 | 248 | if (dataBase[cc]) |
michael@0 | 249 | return cc; |
michael@0 | 250 | } |
michael@0 | 251 | return null; |
michael@0 | 252 | } |
michael@0 | 253 | |
michael@0 | 254 | // Parse an international number that starts with the country code. Return |
michael@0 | 255 | // null if the number is not a valid international number. |
michael@0 | 256 | function ParseInternationalNumber(number) { |
michael@0 | 257 | var ret; |
michael@0 | 258 | |
michael@0 | 259 | // Parse and strip the country code. |
michael@0 | 260 | var countryCode = ParseCountryCode(number); |
michael@0 | 261 | if (!countryCode) |
michael@0 | 262 | return null; |
michael@0 | 263 | number = number.substr(countryCode.length); |
michael@0 | 264 | |
michael@0 | 265 | // Lookup the meta data for the region (or regions) and if the rest of |
michael@0 | 266 | // the number parses for that region, return the parsed number. |
michael@0 | 267 | var entry = dataBase[countryCode]; |
michael@0 | 268 | if (Array.isArray(entry)) { |
michael@0 | 269 | for (var n = 0; n < entry.length; ++n) { |
michael@0 | 270 | if (typeof entry[n] == "string") |
michael@0 | 271 | entry[n] = ParseMetaData(countryCode, entry[n]); |
michael@0 | 272 | if (n > 0) |
michael@0 | 273 | entry[n].formats = entry[0].formats; |
michael@0 | 274 | ret = ParseNationalNumber(number, entry[n]) |
michael@0 | 275 | if (ret) |
michael@0 | 276 | return ret; |
michael@0 | 277 | } |
michael@0 | 278 | return null; |
michael@0 | 279 | } |
michael@0 | 280 | if (typeof entry == "string") |
michael@0 | 281 | entry = dataBase[countryCode] = ParseMetaData(countryCode, entry); |
michael@0 | 282 | return ParseNationalNumber(number, entry); |
michael@0 | 283 | } |
michael@0 | 284 | |
michael@0 | 285 | // Parse a national number for a specific region. Return null if the |
michael@0 | 286 | // number is not a valid national number (it might still be a possible |
michael@0 | 287 | // number for parts of that region). |
michael@0 | 288 | function ParseNationalNumber(number, md) { |
michael@0 | 289 | if (!md.possiblePattern.test(number) || |
michael@0 | 290 | !md.nationalPattern.test(number)) { |
michael@0 | 291 | return null; |
michael@0 | 292 | } |
michael@0 | 293 | // Success. |
michael@0 | 294 | return new NationalNumber(md, number); |
michael@0 | 295 | } |
michael@0 | 296 | |
michael@0 | 297 | // Parse a number and transform it into the national format, removing any |
michael@0 | 298 | // international dial prefixes and country codes. |
michael@0 | 299 | function ParseNumber(number, defaultRegion) { |
michael@0 | 300 | var ret; |
michael@0 | 301 | |
michael@0 | 302 | // Remove formating characters and whitespace. |
michael@0 | 303 | number = PhoneNumberNormalizer.Normalize(number); |
michael@0 | 304 | |
michael@0 | 305 | // If there is no defaultRegion, we can't parse international access codes. |
michael@0 | 306 | if (!defaultRegion && number[0] !== '+') |
michael@0 | 307 | return null; |
michael@0 | 308 | |
michael@0 | 309 | // Detect and strip leading '+'. |
michael@0 | 310 | if (number[0] === '+') |
michael@0 | 311 | return ParseInternationalNumber(number.replace(LEADING_PLUS_CHARS_PATTERN, "")); |
michael@0 | 312 | |
michael@0 | 313 | // Lookup the meta data for the given region. |
michael@0 | 314 | var md = FindMetaDataForRegion(defaultRegion.toUpperCase()); |
michael@0 | 315 | |
michael@0 | 316 | // See if the number starts with an international prefix, and if the |
michael@0 | 317 | // number resulting from stripping the code is valid, then remove the |
michael@0 | 318 | // prefix and flag the number as international. |
michael@0 | 319 | if (md.internationalPrefix.test(number)) { |
michael@0 | 320 | var possibleNumber = number.replace(md.internationalPrefix, ""); |
michael@0 | 321 | ret = ParseInternationalNumber(possibleNumber) |
michael@0 | 322 | if (ret) |
michael@0 | 323 | return ret; |
michael@0 | 324 | } |
michael@0 | 325 | |
michael@0 | 326 | // This is not an international number. See if its a national one for |
michael@0 | 327 | // the current region. National numbers can start with the national |
michael@0 | 328 | // prefix, or without. |
michael@0 | 329 | if (md.nationalPrefixForParsing) { |
michael@0 | 330 | // Some regions have specific national prefix parse rules. Apply those. |
michael@0 | 331 | var withoutPrefix = number.replace(md.nationalPrefixForParsing, |
michael@0 | 332 | md.nationalPrefixTransformRule || ''); |
michael@0 | 333 | ret = ParseNationalNumber(withoutPrefix, md) |
michael@0 | 334 | if (ret) |
michael@0 | 335 | return ret; |
michael@0 | 336 | } else { |
michael@0 | 337 | // If there is no specific national prefix rule, just strip off the |
michael@0 | 338 | // national prefix from the beginning of the number (if there is one). |
michael@0 | 339 | var nationalPrefix = md.nationalPrefix; |
michael@0 | 340 | if (nationalPrefix && number.indexOf(nationalPrefix) == 0 && |
michael@0 | 341 | (ret = ParseNationalNumber(number.substr(nationalPrefix.length), md))) { |
michael@0 | 342 | return ret; |
michael@0 | 343 | } |
michael@0 | 344 | } |
michael@0 | 345 | ret = ParseNationalNumber(number, md) |
michael@0 | 346 | if (ret) |
michael@0 | 347 | return ret; |
michael@0 | 348 | |
michael@0 | 349 | // Now lets see if maybe its an international number after all, but |
michael@0 | 350 | // without '+' or the international prefix. |
michael@0 | 351 | ret = ParseInternationalNumber(number) |
michael@0 | 352 | if (ret) |
michael@0 | 353 | return ret; |
michael@0 | 354 | |
michael@0 | 355 | // If the number matches the possible numbers of the current region, |
michael@0 | 356 | // return it as a possible number. |
michael@0 | 357 | if (md.possiblePattern.test(number)) |
michael@0 | 358 | return new NationalNumber(md, number); |
michael@0 | 359 | |
michael@0 | 360 | // We couldn't parse the number at all. |
michael@0 | 361 | return null; |
michael@0 | 362 | } |
michael@0 | 363 | |
michael@0 | 364 | function IsPlainPhoneNumber(number) { |
michael@0 | 365 | if (typeof number !== 'string') { |
michael@0 | 366 | return false; |
michael@0 | 367 | } |
michael@0 | 368 | |
michael@0 | 369 | var length = number.length; |
michael@0 | 370 | var isTooLong = (length > MAX_PHONE_NUMBER_LENGTH); |
michael@0 | 371 | var isEmpty = (length === 0); |
michael@0 | 372 | return !(isTooLong || isEmpty || NON_DIALABLE_CHARS_ONCE.test(number)); |
michael@0 | 373 | } |
michael@0 | 374 | |
michael@0 | 375 | return { |
michael@0 | 376 | IsPlain: IsPlainPhoneNumber, |
michael@0 | 377 | Parse: ParseNumber, |
michael@0 | 378 | }; |
michael@0 | 379 | })(PHONE_NUMBER_META_DATA); |