1 | <?php |
||||
2 | |||||
3 | namespace libphonenumber\buildtools; |
||||
4 | |||||
5 | use libphonenumber\NumberFormat; |
||||
6 | use libphonenumber\PhoneMetadata; |
||||
7 | use libphonenumber\PhoneNumberDesc; |
||||
8 | |||||
9 | /** |
||||
10 | * Library to build phone number metadata from the XML format. |
||||
11 | * |
||||
12 | * @author Davide Mendolia |
||||
13 | * @internal |
||||
14 | */ |
||||
15 | class BuildMetadataFromXml |
||||
16 | { |
||||
17 | // String constants used to fetch the XML nodes and attributes. |
||||
18 | const CARRIER_CODE_FORMATTING_RULE = 'carrierCodeFormattingRule'; |
||||
19 | const COUNTRY_CODE = 'countryCode'; |
||||
20 | const EMERGENCY = 'emergency'; |
||||
21 | const EXAMPLE_NUMBER = 'exampleNumber'; |
||||
22 | const FIXED_LINE = 'fixedLine'; |
||||
23 | const FORMAT = 'format'; |
||||
24 | const GENERAL_DESC = 'generalDesc'; |
||||
25 | const INTERNATIONAL_PREFIX = 'internationalPrefix'; |
||||
26 | const INTL_FORMAT = 'intlFormat'; |
||||
27 | const LEADING_DIGITS = 'leadingDigits'; |
||||
28 | const MOBILE_NUMBER_PORTABLE_REGION = 'mobileNumberPortableRegion'; |
||||
29 | const MAIN_COUNTRY_FOR_CODE = 'mainCountryForCode'; |
||||
30 | const MOBILE = 'mobile'; |
||||
31 | const NATIONAL_NUMBER_PATTERN = 'nationalNumberPattern'; |
||||
32 | const NATIONAL_PREFIX = 'nationalPrefix'; |
||||
33 | const NATIONAL_PREFIX_FORMATTING_RULE = 'nationalPrefixFormattingRule'; |
||||
34 | const NATIONAL_PREFIX_OPTIONAL_WHEN_FORMATTING = 'nationalPrefixOptionalWhenFormatting'; |
||||
35 | const NATIONAL_PREFIX_FOR_PARSING = 'nationalPrefixForParsing'; |
||||
36 | const NATIONAL_PREFIX_TRANSFORM_RULE = 'nationalPrefixTransformRule'; |
||||
37 | const NO_INTERNATIONAL_DIALLING = 'noInternationalDialling'; |
||||
38 | const NUMBER_FORMAT = 'numberFormat'; |
||||
39 | const PAGER = 'pager'; |
||||
40 | const CARRIER_SPECIFIC = 'carrierSpecific'; |
||||
41 | const PATTERN = 'pattern'; |
||||
42 | const PERSONAL_NUMBER = 'personalNumber'; |
||||
43 | const POSSIBLE_LENGTHS = 'possibleLengths'; |
||||
44 | const NATIONAL = 'national'; |
||||
45 | const LOCAL_ONLY = 'localOnly'; |
||||
46 | const PREFERRED_EXTN_PREFIX = 'preferredExtnPrefix'; |
||||
47 | const PREFERRED_INTERNATIONAL_PREFIX = 'preferredInternationalPrefix'; |
||||
48 | const PREMIUM_RATE = 'premiumRate'; |
||||
49 | const SHARED_COST = 'sharedCost'; |
||||
50 | const SHORT_CODE = 'shortCode'; |
||||
51 | const SMS_SERVICES = 'smsServices'; |
||||
52 | const STANDARD_RATE = 'standardRate'; |
||||
53 | const TOLL_FREE = 'tollFree'; |
||||
54 | const UAN = 'uan'; |
||||
55 | const VOICEMAIL = 'voicemail'; |
||||
56 | const VOIP = 'voip'; |
||||
57 | |||||
58 | private static $phoneNumberDescsWithoutMatchingTypes = array( |
||||
59 | self::NO_INTERNATIONAL_DIALLING |
||||
60 | ); |
||||
61 | |||||
62 | /** |
||||
63 | * @internal |
||||
64 | * @param string $regex |
||||
65 | * @param bool $removeWhitespace |
||||
66 | * @return string |
||||
67 | */ |
||||
68 | public static function validateRE($regex, $removeWhitespace = false) |
||||
69 | { |
||||
70 | $compressedRegex = $removeWhitespace ? \preg_replace('/\\s/', '', $regex) : $regex; |
||||
71 | // Match regex against an empty string to check the regex is valid |
||||
72 | if (\preg_match('/' . $compressedRegex . '/', '') === false) { |
||||
73 | throw new \RuntimeException('Regex error: ' . \preg_last_error()); |
||||
74 | } |
||||
75 | // We don't ever expect to see | followed by a ) in our metadata - this would be an indication |
||||
76 | // of a bug. If one wants to make something optional, we prefer ? to using an empty group. |
||||
77 | $errorIndex = \strpos($compressedRegex, '|)'); |
||||
78 | if ($errorIndex !== false) { |
||||
79 | throw new \RuntimeException('| followed by )'); |
||||
80 | } |
||||
81 | // return the regex if it is of correct syntax, i.e. compile did not fail with a |
||||
82 | return $compressedRegex; |
||||
83 | } |
||||
84 | |||||
85 | /** |
||||
86 | * |
||||
87 | * @param string $inputXmlFile |
||||
88 | * @param boolean $liteBuild |
||||
89 | * @param boolean $specialBuild |
||||
90 | * @param bool $isShortNumberMetadata |
||||
91 | * @param bool $isAlternateFormatsMetadata |
||||
92 | * @return PhoneMetadata[] |
||||
93 | */ |
||||
94 | public static function buildPhoneMetadataCollection($inputXmlFile, $liteBuild, $specialBuild, $isShortNumberMetadata = false, $isAlternateFormatsMetadata = false) |
||||
95 | { |
||||
96 | if ($inputXmlFile instanceof \DOMElement) { |
||||
0 ignored issues
–
show
introduced
by
![]() |
|||||
97 | $document = $inputXmlFile; |
||||
98 | } else { |
||||
99 | $document = new \DOMDocument(); |
||||
100 | $document->load($inputXmlFile); |
||||
101 | $document->normalizeDocument(); |
||||
102 | |||||
103 | $isShortNumberMetadata = \strpos($inputXmlFile, 'ShortNumberMetadata'); |
||||
104 | $isAlternateFormatsMetadata = \strpos($inputXmlFile, 'PhoneNumberAlternateFormats'); |
||||
105 | } |
||||
106 | |||||
107 | $territories = $document->getElementsByTagName('territory'); |
||||
108 | $metadataCollection = array(); |
||||
109 | |||||
110 | $metadataFilter = self::getMetadataFilter($liteBuild, $specialBuild); |
||||
111 | |||||
112 | foreach ($territories as $territoryElement) { |
||||
113 | /** @var $territoryElement \DOMElement */ |
||||
114 | // For the main metadata file this should always be set, but for other supplementary data |
||||
115 | // files the country calling code may be all that is needed. |
||||
116 | if ($territoryElement->hasAttribute('id')) { |
||||
117 | $regionCode = $territoryElement->getAttribute('id'); |
||||
118 | } else { |
||||
119 | $regionCode = ''; |
||||
120 | } |
||||
121 | $metadata = self::loadCountryMetadata($regionCode, $territoryElement, $isShortNumberMetadata, $isAlternateFormatsMetadata); |
||||
122 | $metadataFilter->filterMetadata($metadata); |
||||
123 | $metadataCollection[] = $metadata; |
||||
124 | } |
||||
125 | return $metadataCollection; |
||||
126 | } |
||||
127 | |||||
128 | /** |
||||
129 | * @param string $regionCode |
||||
130 | * @param \DOMElement $element |
||||
131 | * @param string $isShortNumberMetadata |
||||
132 | * @param string $isAlternateFormatsMetadata |
||||
133 | * @return PhoneMetadata |
||||
134 | */ |
||||
135 | public static function loadCountryMetadata($regionCode, \DOMElement $element, $isShortNumberMetadata, $isAlternateFormatsMetadata) |
||||
136 | { |
||||
137 | $nationalPrefix = self::getNationalPrefix($element); |
||||
138 | $metadata = self::loadTerritoryTagMetadata($regionCode, $element, $nationalPrefix); |
||||
139 | $nationalPrefixFormattingRule = self::getNationalPrefixFormattingRuleFromElement($element, $nationalPrefix); |
||||
140 | |||||
141 | self::loadAvailableFormats($metadata, $element, $nationalPrefix, $nationalPrefixFormattingRule, $element->hasAttribute(self::NATIONAL_PREFIX_OPTIONAL_WHEN_FORMATTING)); |
||||
142 | if (!$isAlternateFormatsMetadata) { |
||||
143 | // The alternate formats metadata does not need most of the patterns to be set. |
||||
144 | self::setRelevantDescPatterns($metadata, $element, $isShortNumberMetadata); |
||||
0 ignored issues
–
show
$isShortNumberMetadata of type string is incompatible with the type boolean expected by parameter $isShortNumberMetadata of libphonenumber\buildtool...tRelevantDescPatterns() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
145 | } |
||||
146 | return $metadata; |
||||
147 | } |
||||
148 | |||||
149 | /** |
||||
150 | * Processes the custom build flags and gets a MetadataFilter which may be used to |
||||
151 | * filter PhoneMetadata objects. Incompatible flag combinations throw RuntimeException. |
||||
152 | * @param bool $liteBuild |
||||
153 | * @param bool $specialBuild |
||||
154 | * @return MetadataFilter |
||||
155 | */ |
||||
156 | public static function getMetadataFilter($liteBuild, $specialBuild) |
||||
157 | { |
||||
158 | if ($specialBuild) { |
||||
159 | if ($liteBuild) { |
||||
160 | throw new \RuntimeException('liteBuild and specialBuild may not both be set'); |
||||
161 | } |
||||
162 | return MetadataFilter::forSpecialBuild(); |
||||
163 | } |
||||
164 | if ($liteBuild) { |
||||
165 | return MetadataFilter::forLiteBuild(); |
||||
166 | } |
||||
167 | |||||
168 | return MetadataFilter::emptyFilter(); |
||||
169 | } |
||||
170 | |||||
171 | /** |
||||
172 | * Returns the national prefix of the provided country element. |
||||
173 | * @internal |
||||
174 | * @param \DOMElement $element |
||||
175 | * @return string |
||||
176 | */ |
||||
177 | public static function getNationalPrefix(\DOMElement $element) |
||||
178 | { |
||||
179 | return $element->hasAttribute(self::NATIONAL_PREFIX) ? $element->getAttribute(self::NATIONAL_PREFIX) : ''; |
||||
180 | } |
||||
181 | |||||
182 | /** |
||||
183 | * |
||||
184 | * @internal |
||||
185 | * @param \DOMElement $element |
||||
186 | * @param string $nationalPrefix |
||||
187 | * @return string |
||||
188 | */ |
||||
189 | public static function getNationalPrefixFormattingRuleFromElement(\DOMElement $element, $nationalPrefix) |
||||
190 | { |
||||
191 | $nationalPrefixFormattingRule = $element->getAttribute(self::NATIONAL_PREFIX_FORMATTING_RULE); |
||||
192 | // Replace $NP with national prefix and $FG with the first group ($1). |
||||
193 | $nationalPrefixFormattingRule = \str_replace( |
||||
194 | array('$NP', '$FG'), |
||||
195 | array($nationalPrefix, '$1'), |
||||
196 | $nationalPrefixFormattingRule |
||||
197 | ); |
||||
198 | return $nationalPrefixFormattingRule; |
||||
199 | } |
||||
200 | |||||
201 | /** |
||||
202 | * |
||||
203 | * @internal |
||||
204 | * @param string $regionCode |
||||
205 | * @param \DOMElement $element |
||||
206 | * @param string $nationalPrefix |
||||
207 | * @return PhoneMetadata |
||||
208 | */ |
||||
209 | public static function loadTerritoryTagMetadata( |
||||
210 | $regionCode, |
||||
211 | \DOMElement $element, |
||||
212 | $nationalPrefix |
||||
213 | ) { |
||||
214 | $metadata = new PhoneMetadata(); |
||||
215 | $metadata->setId($regionCode); |
||||
216 | $metadata->setCountryCode((int)$element->getAttribute(self::COUNTRY_CODE)); |
||||
217 | if ($element->hasAttribute(self::LEADING_DIGITS)) { |
||||
218 | $metadata->setLeadingDigits(self::validateRE($element->getAttribute(self::LEADING_DIGITS))); |
||||
219 | } |
||||
220 | $metadata->setInternationalPrefix(self::validateRE($element->getAttribute(self::INTERNATIONAL_PREFIX))); |
||||
221 | if ($element->hasAttribute(self::PREFERRED_INTERNATIONAL_PREFIX)) { |
||||
222 | $preferredInternationalPrefix = $element->getAttribute(self::PREFERRED_INTERNATIONAL_PREFIX); |
||||
223 | $metadata->setPreferredInternationalPrefix($preferredInternationalPrefix); |
||||
224 | } |
||||
225 | if ($element->hasAttribute(self::NATIONAL_PREFIX_FOR_PARSING)) { |
||||
226 | $metadata->setNationalPrefixForParsing( |
||||
227 | self::validateRE($element->getAttribute(self::NATIONAL_PREFIX_FOR_PARSING), true) |
||||
228 | ); |
||||
229 | if ($element->hasAttribute(self::NATIONAL_PREFIX_TRANSFORM_RULE)) { |
||||
230 | $metadata->setNationalPrefixTransformRule(self::validateRE($element->getAttribute(self::NATIONAL_PREFIX_TRANSFORM_RULE))); |
||||
231 | } |
||||
232 | } |
||||
233 | if ($nationalPrefix != '') { |
||||
234 | $metadata->setNationalPrefix($nationalPrefix); |
||||
235 | if (!$metadata->hasNationalPrefixForParsing()) { |
||||
236 | $metadata->setNationalPrefixForParsing($nationalPrefix); |
||||
237 | } |
||||
238 | } |
||||
239 | if ($element->hasAttribute(self::PREFERRED_EXTN_PREFIX)) { |
||||
240 | $metadata->setPreferredExtnPrefix($element->getAttribute(self::PREFERRED_EXTN_PREFIX)); |
||||
241 | } |
||||
242 | if ($element->hasAttribute(self::MAIN_COUNTRY_FOR_CODE)) { |
||||
243 | $metadata->setMainCountryForCode(true); |
||||
244 | } |
||||
245 | if ($element->hasAttribute(self::MOBILE_NUMBER_PORTABLE_REGION)) { |
||||
246 | $metadata->setMobileNumberPortableRegion(true); |
||||
247 | } |
||||
248 | return $metadata; |
||||
249 | } |
||||
250 | |||||
251 | /** |
||||
252 | * Extracts the available formats from the provided DOM element. If it does not contain any |
||||
253 | * nationalPrefixFormattingRule, the one passed-in is retained; similarly for |
||||
254 | * nationalPrefixOptionalWhenFormatting. The nationalPrefix, nationalPrefixFormattingRule and |
||||
255 | * nationalPrefixOptionalWhenFormatting values are provided from the parent (territory) element. |
||||
256 | * @internal |
||||
257 | * @param PhoneMetadata $metadata |
||||
258 | * @param \DOMElement $element |
||||
259 | * @param string $nationalPrefix |
||||
260 | * @param string $nationalPrefixFormattingRule |
||||
261 | * @param bool $nationalPrefixOptionalWhenFormatting |
||||
262 | */ |
||||
263 | public static function loadAvailableFormats( |
||||
264 | PhoneMetadata $metadata, |
||||
265 | \DOMElement $element, |
||||
266 | $nationalPrefix, |
||||
267 | $nationalPrefixFormattingRule, |
||||
268 | $nationalPrefixOptionalWhenFormatting |
||||
269 | ) { |
||||
270 | $carrierCodeFormattingRule = ''; |
||||
271 | if ($element->hasAttribute(self::CARRIER_CODE_FORMATTING_RULE)) { |
||||
272 | $carrierCodeFormattingRule = self::validateRE(self::getDomesticCarrierCodeFormattingRuleFromElement($element, $nationalPrefix)); |
||||
273 | } |
||||
274 | $numberFormatElements = $element->getElementsByTagName(self::NUMBER_FORMAT); |
||||
275 | $hasExplicitIntlFormatDefined = false; |
||||
276 | |||||
277 | $numOfFormatElements = $numberFormatElements->length; |
||||
278 | if ($numOfFormatElements > 0) { |
||||
279 | for ($i = 0; $i < $numOfFormatElements; $i++) { |
||||
280 | /** @var \DOMElement $numberFormatElement */ |
||||
281 | $numberFormatElement = $numberFormatElements->item($i); |
||||
282 | $format = new NumberFormat(); |
||||
283 | |||||
284 | if ($numberFormatElement->hasAttribute(self::NATIONAL_PREFIX_FORMATTING_RULE)) { |
||||
285 | $format->setNationalPrefixFormattingRule( |
||||
286 | self::getNationalPrefixFormattingRuleFromElement($numberFormatElement, $nationalPrefix) |
||||
287 | ); |
||||
288 | } else { |
||||
289 | $format->setNationalPrefixFormattingRule($nationalPrefixFormattingRule); |
||||
290 | } |
||||
291 | if ($numberFormatElement->hasAttribute(self::NATIONAL_PREFIX_OPTIONAL_WHEN_FORMATTING)) { |
||||
292 | $format->setNationalPrefixOptionalWhenFormatting($numberFormatElement->getAttribute(self::NATIONAL_PREFIX_OPTIONAL_WHEN_FORMATTING) === 'true' ? true : false); |
||||
293 | } else { |
||||
294 | $format->setNationalPrefixOptionalWhenFormatting($nationalPrefixOptionalWhenFormatting); |
||||
295 | } |
||||
296 | if ($numberFormatElement->hasAttribute(self::CARRIER_CODE_FORMATTING_RULE)) { |
||||
297 | $format->setDomesticCarrierCodeFormattingRule( |
||||
298 | self::validateRE(self::getDomesticCarrierCodeFormattingRuleFromElement($numberFormatElement, $nationalPrefix)) |
||||
299 | ); |
||||
300 | } else { |
||||
301 | $format->setDomesticCarrierCodeFormattingRule($carrierCodeFormattingRule); |
||||
302 | } |
||||
303 | self::loadNationalFormat($metadata, $numberFormatElement, $format); |
||||
304 | $metadata->addNumberFormat($format); |
||||
305 | |||||
306 | if (self::loadInternationalFormat($metadata, $numberFormatElement, $format)) { |
||||
307 | $hasExplicitIntlFormatDefined = true; |
||||
308 | } |
||||
309 | } |
||||
310 | // Only a small number of regions need to specify the intlFormats in the xml. For the majority |
||||
311 | // of countries the intlNumberFormat metadata is an exact copy of the national NumberFormat |
||||
312 | // metadata. To minimize the size of the metadata file, we only keep intlNumberFormats that |
||||
313 | // actually differ in some way to the national formats. |
||||
314 | if (!$hasExplicitIntlFormatDefined) { |
||||
0 ignored issues
–
show
|
|||||
315 | $metadata->clearIntlNumberFormat(); |
||||
316 | } |
||||
317 | } |
||||
318 | } |
||||
319 | |||||
320 | /** |
||||
321 | * @internal |
||||
322 | * @param \DOMElement $element |
||||
323 | * @param string $nationalPrefix |
||||
324 | * @return string |
||||
325 | */ |
||||
326 | public static function getDomesticCarrierCodeFormattingRuleFromElement(\DOMElement $element, $nationalPrefix) |
||||
327 | { |
||||
328 | $carrierCodeFormattingRule = $element->getAttribute(self::CARRIER_CODE_FORMATTING_RULE); |
||||
329 | // Replace $FG with the first group ($1) and $NP with the national prefix. |
||||
330 | $carrierCodeFormattingRule = \str_replace( |
||||
331 | array('$NP', '$FG'), |
||||
332 | array($nationalPrefix, '$1'), |
||||
333 | $carrierCodeFormattingRule |
||||
334 | ); |
||||
335 | return $carrierCodeFormattingRule; |
||||
336 | } |
||||
337 | |||||
338 | /** |
||||
339 | * Extracts the pattern for the national format. |
||||
340 | * |
||||
341 | * @internal |
||||
342 | * @param PhoneMetadata $metadata |
||||
343 | * @param \DOMElement $numberFormatElement |
||||
344 | * @param NumberFormat $format |
||||
345 | * @throws \RuntimeException if multiple or no formats have been encountered. |
||||
346 | */ |
||||
347 | public static function loadNationalFormat( |
||||
348 | PhoneMetadata $metadata, |
||||
349 | \DOMElement $numberFormatElement, |
||||
350 | NumberFormat $format |
||||
351 | ) { |
||||
352 | self::setLeadingDigitsPatterns($numberFormatElement, $format); |
||||
353 | $format->setPattern(self::validateRE($numberFormatElement->getAttribute(self::PATTERN))); |
||||
354 | |||||
355 | $formatPattern = $numberFormatElement->getElementsByTagName(self::FORMAT); |
||||
356 | if ($formatPattern->length != 1) { |
||||
357 | $countryId = $metadata->getId() != '' ? $metadata->getId() : $metadata->getCountryCode(); |
||||
358 | throw new \RuntimeException('Invalid number of format patterns for country: ' . $countryId); |
||||
359 | } |
||||
360 | $nationalFormat = $formatPattern->item(0)->firstChild->nodeValue; |
||||
361 | $format->setFormat($nationalFormat); |
||||
362 | } |
||||
363 | |||||
364 | /** |
||||
365 | * @internal |
||||
366 | * @param \DOMElement $numberFormatElement |
||||
367 | * @param NumberFormat $format |
||||
368 | */ |
||||
369 | public static function setLeadingDigitsPatterns(\DOMElement $numberFormatElement, NumberFormat $format) |
||||
370 | { |
||||
371 | $leadingDigitsPatternNodes = $numberFormatElement->getElementsByTagName(self::LEADING_DIGITS); |
||||
372 | $numOfLeadingDigitsPatterns = $leadingDigitsPatternNodes->length; |
||||
373 | if ($numOfLeadingDigitsPatterns > 0) { |
||||
374 | for ($i = 0; $i < $numOfLeadingDigitsPatterns; $i++) { |
||||
375 | $format->addLeadingDigitsPattern(self::validateRE($leadingDigitsPatternNodes->item($i)->firstChild->nodeValue, true)); |
||||
376 | } |
||||
377 | } |
||||
378 | } |
||||
379 | |||||
380 | /** |
||||
381 | * Extracts the pattern for international format. If there is no intlFormat, default to using the |
||||
382 | * national format. If the intlFormat is set to "NA" the intlFormat should be ignored. |
||||
383 | * |
||||
384 | * @internal |
||||
385 | * @param PhoneMetadata $metadata |
||||
386 | * @param \DOMElement $numberFormatElement |
||||
387 | * @param NumberFormat $nationalFormat |
||||
388 | * @throws \RuntimeException if multiple intlFormats have been encountered. |
||||
389 | * @return bool whether an international number format is defined. |
||||
390 | */ |
||||
391 | public static function loadInternationalFormat( |
||||
392 | PhoneMetadata $metadata, |
||||
393 | \DOMElement $numberFormatElement, |
||||
394 | NumberFormat $nationalFormat |
||||
395 | ) { |
||||
396 | $intlFormat = new NumberFormat(); |
||||
397 | $intlFormatPattern = $numberFormatElement->getElementsByTagName(self::INTL_FORMAT); |
||||
398 | $hasExplicitIntlFormatDefined = false; |
||||
399 | |||||
400 | if ($intlFormatPattern->length > 1) { |
||||
401 | $countryId = $metadata->getId() != '' ? $metadata->getId() : $metadata->getCountryCode(); |
||||
402 | throw new \RuntimeException('Invalid number of intlFormat patterns for country: ' . $countryId); |
||||
403 | } |
||||
404 | |||||
405 | if ($intlFormatPattern->length === 0) { |
||||
406 | // Default to use the same as the national pattern if none is defined. |
||||
407 | $intlFormat->mergeFrom($nationalFormat); |
||||
408 | } else { |
||||
409 | $intlFormat->setPattern($numberFormatElement->getAttribute(self::PATTERN)); |
||||
410 | self::setLeadingDigitsPatterns($numberFormatElement, $intlFormat); |
||||
411 | $intlFormatPatternValue = $intlFormatPattern->item(0)->firstChild->nodeValue; |
||||
412 | if ($intlFormatPatternValue !== 'NA') { |
||||
413 | $intlFormat->setFormat($intlFormatPatternValue); |
||||
414 | } |
||||
415 | $hasExplicitIntlFormatDefined = true; |
||||
416 | } |
||||
417 | |||||
418 | if ($intlFormat->hasFormat()) { |
||||
419 | $metadata->addIntlNumberFormat($intlFormat); |
||||
420 | } |
||||
421 | return $hasExplicitIntlFormatDefined; |
||||
422 | } |
||||
423 | |||||
424 | /** |
||||
425 | * @internal |
||||
426 | * @param PhoneMetadata $metadata |
||||
427 | * @param \DOMElement $element |
||||
428 | * @param bool $isShortNumberMetadata |
||||
429 | */ |
||||
430 | public static function setRelevantDescPatterns(PhoneMetadata $metadata, \DOMElement $element, $isShortNumberMetadata) |
||||
431 | { |
||||
432 | $generalDesc = self::processPhoneNumberDescElement(null, $element, self::GENERAL_DESC); |
||||
433 | $metadata->setGeneralDesc($generalDesc); |
||||
434 | |||||
435 | $metadataId = $metadata->getId(); |
||||
436 | // Calculate the possible lengths for the general description. This will be based on the |
||||
437 | // possible lengths of the child elements. |
||||
438 | self::setPossibleLengthsGeneralDesc($generalDesc, $metadataId, $element, $isShortNumberMetadata); |
||||
439 | |||||
440 | if (!$isShortNumberMetadata) { |
||||
441 | // Set fields used by regular length phone numbers. |
||||
442 | $metadata->setFixedLine(self::processPhoneNumberDescElement($generalDesc, $element, self::FIXED_LINE)); |
||||
443 | $metadata->setMobile(self::processPhoneNumberDescElement($generalDesc, $element, self::MOBILE)); |
||||
444 | $metadata->setSharedCost(self::processPhoneNumberDescElement($generalDesc, $element, self::SHARED_COST)); |
||||
445 | $metadata->setVoip(self::processPhoneNumberDescElement($generalDesc, $element, self::VOIP)); |
||||
446 | $metadata->setPersonalNumber(self::processPhoneNumberDescElement($generalDesc, $element, self::PERSONAL_NUMBER)); |
||||
447 | $metadata->setPager(self::processPhoneNumberDescElement($generalDesc, $element, self::PAGER)); |
||||
448 | $metadata->setUan(self::processPhoneNumberDescElement($generalDesc, $element, self::UAN)); |
||||
449 | $metadata->setVoicemail(self::processPhoneNumberDescElement($generalDesc, $element, self::VOICEMAIL)); |
||||
450 | $metadata->setNoInternationalDialling(self::processPhoneNumberDescElement($generalDesc, $element, self::NO_INTERNATIONAL_DIALLING)); |
||||
451 | $metadata->setSameMobileAndFixedLinePattern($metadata->getMobile()->getNationalNumberPattern() === $metadata->getFixedLine()->getNationalNumberPattern()); |
||||
452 | $metadata->setTollFree(self::processPhoneNumberDescElement($generalDesc, $element, self::TOLL_FREE)); |
||||
453 | $metadata->setPremiumRate(self::processPhoneNumberDescElement($generalDesc, $element, self::PREMIUM_RATE)); |
||||
454 | } else { |
||||
455 | // Set fields used by short numbers. |
||||
456 | $metadata->setStandardRate(self::processPhoneNumberDescElement($generalDesc, $element, self::STANDARD_RATE)); |
||||
457 | $metadata->setShortCode(self::processPhoneNumberDescElement($generalDesc, $element, self::SHORT_CODE)); |
||||
458 | $metadata->setCarrierSpecific(self::processPhoneNumberDescElement($generalDesc, $element, self::CARRIER_SPECIFIC)); |
||||
459 | $metadata->setEmergency(self::processPhoneNumberDescElement($generalDesc, $element, self::EMERGENCY)); |
||||
460 | $metadata->setTollFree(self::processPhoneNumberDescElement($generalDesc, $element, self::TOLL_FREE)); |
||||
461 | $metadata->setPremiumRate(self::processPhoneNumberDescElement($generalDesc, $element, self::PREMIUM_RATE)); |
||||
462 | $metadata->setSmsServices(self::processPhoneNumberDescElement($generalDesc, $element, self::SMS_SERVICES)); |
||||
463 | } |
||||
464 | } |
||||
465 | |||||
466 | /** |
||||
467 | * Parses a possible length string into a set of the integers that are covered. |
||||
468 | * |
||||
469 | * @param string $possibleLengthString a string specifying the possible lengths of phone numbers. Follows |
||||
470 | * this syntax: ranges or elements are separated by commas, and ranges are specified in |
||||
471 | * [min-max] notation, inclusive. For example, [3-5],7,9,[11-14] should be parsed to |
||||
472 | * 3,4,5,7,9,11,12,13,14 |
||||
473 | * @return array |
||||
474 | */ |
||||
475 | private static function parsePossibleLengthStringToSet($possibleLengthString) |
||||
476 | { |
||||
477 | if (\strlen($possibleLengthString) === 0) { |
||||
478 | throw new \RuntimeException('Empty possibleLength string found.'); |
||||
479 | } |
||||
480 | |||||
481 | $lengths = \explode(',', $possibleLengthString); |
||||
482 | $lengthSet = array(); |
||||
483 | |||||
484 | |||||
485 | $lengthLength = \count($lengths); |
||||
486 | for ($i = 0; $i < $lengthLength; $i++) { |
||||
487 | $lengthSubstring = $lengths[$i]; |
||||
488 | if (\strlen($lengthSubstring) === 0) { |
||||
489 | throw new \RuntimeException('Leading, trailing or adjacent commas in possible ' |
||||
490 | . "length string {$possibleLengthString}, these should only separate numbers or ranges."); |
||||
491 | } |
||||
492 | |||||
493 | if (\substr($lengthSubstring, 0, 1) === '[') { |
||||
494 | if (\substr($lengthSubstring, -1) !== ']') { |
||||
495 | throw new \RuntimeException("Missing end of range character in possible length string {$possibleLengthString}."); |
||||
496 | } |
||||
497 | |||||
498 | // Strip the leading and trailing [], and split on the -. |
||||
499 | $minMax = \explode('-', \substr($lengthSubstring, 1, -1)); |
||||
500 | if (\count($minMax) !== 2) { |
||||
501 | throw new \RuntimeException("Ranges must have exactly one - character: missing for {$possibleLengthString}."); |
||||
502 | } |
||||
503 | $min = (int)$minMax[0]; |
||||
504 | $max = (int)$minMax[1]; |
||||
505 | // We don't even accept [6-7] since we prefer the shorter 6,7 variant; for a range to be in |
||||
506 | // use the hyphen needs to replace at least one digit. |
||||
507 | if ($max - $min < 2) { |
||||
508 | throw new \RuntimeException("The first number in a range should be two or more digits lower than the second. Culprit possibleLength string: {$possibleLengthString}."); |
||||
509 | } |
||||
510 | for ($j = $min; $j <= $max; $j++) { |
||||
511 | if (\in_array($j, $lengthSet)) { |
||||
512 | throw new \RuntimeException("Duplicate length element found ({$j}) in possibleLength string {$possibleLengthString}."); |
||||
513 | } |
||||
514 | $lengthSet[] = $j; |
||||
515 | } |
||||
516 | } else { |
||||
517 | $length = $lengthSubstring; |
||||
518 | if (\in_array($length, $lengthSet)) { |
||||
519 | throw new \RuntimeException("Duplicate length element found ({$length}) in possibleLength string {$possibleLengthString}."); |
||||
520 | } |
||||
521 | if (!\is_numeric($length)) { |
||||
522 | throw new \RuntimeException("For input string \"{$length}\""); |
||||
523 | } |
||||
524 | $lengthSet[] = (int)$length; |
||||
525 | } |
||||
526 | } |
||||
527 | return $lengthSet; |
||||
528 | } |
||||
529 | |||||
530 | /** |
||||
531 | * Reads the possible length present in the metadata and splits them into two sets: one for |
||||
532 | * full-length numbers, one for local numbers. |
||||
533 | * |
||||
534 | * |
||||
535 | * @param \DOMElement $data One or more phone number descriptions |
||||
536 | * @param array $lengths An array in which to add possible lengths of full phone numbers |
||||
537 | * @param array $localOnlyLengths An array in which to add possible lengths of phone numbers only diallable |
||||
538 | * locally (e.g. within a province) |
||||
539 | */ |
||||
540 | private static function populatePossibleLengthSets(\DOMElement $data, &$lengths, &$localOnlyLengths) |
||||
541 | { |
||||
542 | $possibleLengths = $data->getElementsByTagName(self::POSSIBLE_LENGTHS); |
||||
543 | |||||
544 | for ($i = 0; $i < $possibleLengths->length; $i++) { |
||||
545 | /** @var \DOMElement $element */ |
||||
546 | $element = $possibleLengths->item($i); |
||||
547 | $nationalLengths = $element->getAttribute(self::NATIONAL); |
||||
548 | // We don't add to the phone metadata yet, since we want to sort length elements found under |
||||
549 | // different nodes first, make sure there are no duplicates between them and that the |
||||
550 | // localOnly lengths don't overlap with the others. |
||||
551 | $thisElementLengths = self::parsePossibleLengthStringToSet($nationalLengths); |
||||
552 | if ($element->hasAttribute(self::LOCAL_ONLY)) { |
||||
553 | $localLengths = $element->getAttribute(self::LOCAL_ONLY); |
||||
554 | $thisElementLocalOnlyLengths = self::parsePossibleLengthStringToSet($localLengths); |
||||
555 | $intersection = \array_intersect($thisElementLengths, $thisElementLocalOnlyLengths); |
||||
556 | if (\count($intersection) > 0) { |
||||
557 | throw new \RuntimeException('Possible length(s) found specified as a normal and local-only length: [' . \implode(',', $intersection) . '].'); |
||||
558 | } |
||||
559 | // We check again when we set these lengths on the metadata itself in setPossibleLengths |
||||
560 | // that the elements in localOnly are not also in lengths. For e.g. the generalDesc, it |
||||
561 | // might have a local-only length for one type that is a normal length for another type. We |
||||
562 | // don't consider this an error, but we do want to remove the local-only lengths. |
||||
563 | $localOnlyLengths = \array_merge($localOnlyLengths, $thisElementLocalOnlyLengths); |
||||
564 | \sort($localOnlyLengths); |
||||
565 | } |
||||
566 | // It is okay if at this time we have duplicates, because the same length might be possible |
||||
567 | // for e.g. fixed-line and for mobile numbers, and this method operates potentially on |
||||
568 | // multiple phoneNumberDesc XML elements. |
||||
569 | $lengths = \array_merge($lengths, $thisElementLengths); |
||||
570 | \sort($lengths); |
||||
571 | } |
||||
572 | } |
||||
573 | |||||
574 | /** |
||||
575 | * Sets possible lengths in the general description, derived from certain child elements |
||||
576 | * |
||||
577 | * @internal |
||||
578 | * @param PhoneNumberDesc $generalDesc |
||||
579 | * @param string $metadataId |
||||
580 | * @param \DOMElement $data |
||||
581 | * @param bool $isShortNumberMetadata |
||||
582 | */ |
||||
583 | public static function setPossibleLengthsGeneralDesc(PhoneNumberDesc $generalDesc, $metadataId, \DOMElement $data, $isShortNumberMetadata) |
||||
584 | { |
||||
585 | $lengths = array(); |
||||
586 | $localOnlyLengths = array(); |
||||
587 | // The general description node should *always* be present if metadata for other types is |
||||
588 | // present, aside from in some unit tests. |
||||
589 | // (However, for e.g. formatting metadata in PhoneNumberAlternateFormats, no PhoneNumberDesc |
||||
590 | // elements are present). |
||||
591 | $generalDescNodes = $data->getElementsByTagName(self::GENERAL_DESC); |
||||
592 | if ($generalDescNodes->length > 0) { |
||||
593 | $generalDescNode = $generalDescNodes->item(0); |
||||
594 | self::populatePossibleLengthSets($generalDescNode, $lengths, $localOnlyLengths); |
||||
595 | if (\count($lengths) > 0 || \count($localOnlyLengths) > 0) { |
||||
596 | // We shouldn't have anything specified at the "general desc" level: we are going to |
||||
597 | // calculate this ourselves from child elements. |
||||
598 | throw new \RuntimeException("Found possible lengths specified at general desc: this should be derived from child elements. Affected country: {$metadataId}"); |
||||
599 | } |
||||
600 | } |
||||
601 | if (!$isShortNumberMetadata) { |
||||
602 | // Make a copy here since we want to remove some nodes, but we don't want to do that on our |
||||
603 | // actual data. |
||||
604 | /** @var \DOMElement $allDescData */ |
||||
605 | $allDescData = $data->cloneNode(true); |
||||
606 | foreach (self::$phoneNumberDescsWithoutMatchingTypes as $tag) { |
||||
607 | $nodesToRemove = $allDescData->getElementsByTagName($tag); |
||||
608 | if ($nodesToRemove->length > 0) { |
||||
609 | // We check when we process phone number descriptions that there are only one of each |
||||
610 | // type, so this is safe to do. |
||||
611 | $allDescData->removeChild($nodesToRemove->item(0)); |
||||
612 | } |
||||
613 | } |
||||
614 | self::populatePossibleLengthSets($allDescData, $lengths, $localOnlyLengths); |
||||
615 | } else { |
||||
616 | // For short number metadata, we want to copy the lengths from the "short code" section only. |
||||
617 | // This is because it's the more detailed validation pattern, it's not a sub-type of short |
||||
618 | // codes. The other lengths will be checked later to see that they are a sub-set of these |
||||
619 | // possible lengths. |
||||
620 | $shortCodeDescList = $data->getElementsByTagName(self::SHORT_CODE); |
||||
621 | if (\count($shortCodeDescList) > 0) { |
||||
622 | $shortCodeDesc = $shortCodeDescList->item(0); |
||||
623 | self::populatePossibleLengthSets($shortCodeDesc, $lengths, $localOnlyLengths); |
||||
624 | } |
||||
625 | if (\count($localOnlyLengths) > 0) { |
||||
626 | throw new \RuntimeException('Found local-only lengths in short-number metadata'); |
||||
627 | } |
||||
628 | } |
||||
629 | self::setPossibleLengths($lengths, $localOnlyLengths, null, $generalDesc); |
||||
630 | } |
||||
631 | |||||
632 | /** |
||||
633 | * Sets the possible length fields in the metadata from the sets of data passed in. Checks that |
||||
634 | * the length is covered by the "parent" phone number description element if one is present, and |
||||
635 | * if the lengths are exactly the same as this, they are not filled in for efficiency reasons. |
||||
636 | * |
||||
637 | * @param array $lengths |
||||
638 | * @param array $localOnlyLengths |
||||
639 | * @param PhoneNumberDesc $parentDesc |
||||
640 | * @param PhoneNumberDesc $desc |
||||
641 | */ |
||||
642 | private static function setPossibleLengths($lengths, $localOnlyLengths, PhoneNumberDesc $parentDesc = null, PhoneNumberDesc $desc) |
||||
643 | { |
||||
644 | // We clear these fields since the metadata tends to inherit from the parent element for other |
||||
645 | // fields (via a mergeFrom). |
||||
646 | $desc->clearPossibleLength(); |
||||
647 | $desc->clearPossibleLengthLocalOnly(); |
||||
648 | |||||
649 | // Only add the lengths to this sub-type if they aren't exactly the same as the possible |
||||
650 | // lengths in the general desc (for metadata size reasons). |
||||
651 | if ($parentDesc === null || !self::arePossibleLengthsEqual($lengths, $parentDesc)) { |
||||
652 | foreach ($lengths as $length) { |
||||
653 | if ($parentDesc === null || \in_array($length, $parentDesc->getPossibleLength())) { |
||||
654 | $desc->addPossibleLength($length); |
||||
655 | } else { |
||||
656 | // We shouldn't have possible lengths defined in a child element that are not covered by |
||||
657 | // the general description. We check this here even though the general description is |
||||
658 | // derived from child elements because it is only derived from a subset, and we need to |
||||
659 | // ensure *all* child elements have a valid possible length. |
||||
660 | throw new \RuntimeException("Out-of-range possible length found ({$length}), parent lengths " . \implode(',', $parentDesc->getPossibleLength())); |
||||
661 | } |
||||
662 | } |
||||
663 | } |
||||
664 | // We check that the local-only length isn't also a normal possible length (only relevant for |
||||
665 | // the general-desc, since within elements such as fixed-line we would throw an exception if we |
||||
666 | // saw this) before adding it to the collection of possible local-only lengths. |
||||
667 | foreach ($localOnlyLengths as $length) { |
||||
668 | if (!\in_array($length, $lengths)) { |
||||
669 | // We check it is covered by either of the possible length sets of the parent |
||||
670 | // PhoneNumberDesc, because for example 7 might be a valid localOnly length for mobile, but |
||||
671 | // a valid national length for fixedLine, so the generalDesc would have the 7 removed from |
||||
672 | // localOnly. |
||||
673 | if ($parentDesc === null |
||||
674 | || \in_array($length, $parentDesc->getPossibleLength()) |
||||
675 | || \in_array($length, $parentDesc->getPossibleLengthLocalOnly()) |
||||
676 | ) { |
||||
677 | $desc->addPossibleLengthLocalOnly($length); |
||||
678 | } else { |
||||
679 | throw new \RuntimeException("Out-of-range local-only possible length found ({$length}), parent length {$parentDesc->getPossibleLengthLocalOnly()}"); |
||||
680 | } |
||||
681 | } |
||||
682 | } |
||||
683 | } |
||||
684 | |||||
685 | /** |
||||
686 | * Processes a phone number description element from the XML file and returns it as a |
||||
687 | * PhoneNumberDesc. If the description element is a fixed line or mobile number, the parent |
||||
688 | * description will be used to fill in the whole element if necessary, or any components that are |
||||
689 | * missing. For all other types, the parent description will only be used to fill in missing |
||||
690 | * components if the type has a partial definition. For example, if no "tollFree" element exists, |
||||
691 | * we assume there are no toll free numbers for that locale, and return a phone number description |
||||
692 | * with no national number data and [-1] for this possible lengths. Note that the parent |
||||
693 | * description must therefore already be processed before this method is called on any child |
||||
694 | * elements. |
||||
695 | * |
||||
696 | * @internal |
||||
697 | * @param PhoneNumberDesc $parentDesc a generic phone number description that will be used to fill in missing |
||||
698 | * parts of the description, or null if this is the root node. This must be processed before |
||||
699 | * this is run on any child elements. |
||||
700 | * @param \DOMElement $countryElement XML element representing all the country information |
||||
701 | * @param string $numberType name of the number type, corresponding to the appropriate tag in the XML |
||||
702 | * file with information about that type |
||||
703 | * @return PhoneNumberDesc complete description of that phone number type |
||||
704 | */ |
||||
705 | public static function processPhoneNumberDescElement( |
||||
706 | PhoneNumberDesc $parentDesc = null, |
||||
707 | \DOMElement $countryElement, |
||||
708 | $numberType |
||||
709 | ) { |
||||
710 | $phoneNumberDescList = $countryElement->getElementsByTagName($numberType); |
||||
711 | $numberDesc = new PhoneNumberDesc(); |
||||
712 | if ($phoneNumberDescList->length == 0) { |
||||
713 | // -1 will never match a possible phone number length, so is safe to use to ensure this never |
||||
714 | // matches. We don't leave it empty, since for compression reasons, we use the empty list to |
||||
715 | // mean that the generalDesc possible lengths apply. |
||||
716 | $numberDesc->setPossibleLength(array(-1)); |
||||
717 | return $numberDesc; |
||||
718 | } |
||||
719 | |||||
720 | if ($parentDesc != null) { |
||||
721 | if ($parentDesc->getNationalNumberPattern() !== '') { |
||||
722 | $numberDesc->setNationalNumberPattern($parentDesc->getNationalNumberPattern()); |
||||
723 | } |
||||
724 | if ($parentDesc->getExampleNumber() !== '') { |
||||
725 | $numberDesc->setExampleNumber($parentDesc->getExampleNumber()); |
||||
726 | } |
||||
727 | } |
||||
728 | |||||
729 | if ($phoneNumberDescList->length > 0) { |
||||
730 | if ($phoneNumberDescList->length > 1) { |
||||
731 | throw new \RuntimeException("Multiple elements with type {$numberType} found."); |
||||
732 | } |
||||
733 | |||||
734 | /** @var \DOMElement $element */ |
||||
735 | $element = $phoneNumberDescList->item(0); |
||||
736 | |||||
737 | if ($parentDesc != null) { |
||||
738 | // New way of handling possible number lengths. We don't do this for the general |
||||
739 | // description, since these tags won't be present; instead we will calculate its values |
||||
740 | // based on the values for all the other number type descriptions (see |
||||
741 | // setPossibleLengthsGeneralDesc). |
||||
742 | $lengths = array(); |
||||
743 | $localOnlyLengths = array(); |
||||
744 | self::populatePossibleLengthSets($element, $lengths, $localOnlyLengths); |
||||
745 | self::setPossibleLengths($lengths, $localOnlyLengths, $parentDesc, $numberDesc); |
||||
746 | } |
||||
747 | |||||
748 | $validPattern = $element->getElementsByTagName(self::NATIONAL_NUMBER_PATTERN); |
||||
749 | if ($validPattern->length > 0) { |
||||
750 | $numberDesc->setNationalNumberPattern(self::validateRE($validPattern->item(0)->firstChild->nodeValue, true)); |
||||
751 | } |
||||
752 | |||||
753 | $exampleNumber = $element->getElementsByTagName(self::EXAMPLE_NUMBER); |
||||
754 | if ($exampleNumber->length > 0) { |
||||
755 | $numberDesc->setExampleNumber($exampleNumber->item(0)->firstChild->nodeValue); |
||||
756 | } |
||||
757 | } |
||||
758 | return $numberDesc; |
||||
759 | } |
||||
760 | |||||
761 | private static function arePossibleLengthsEqual($possibleLengths, PhoneNumberDesc $desc) |
||||
762 | { |
||||
763 | $descPossibleLength = $desc->getPossibleLength(); |
||||
764 | if (\count($possibleLengths) != \count($descPossibleLength)) { |
||||
765 | return false; |
||||
766 | } |
||||
767 | |||||
768 | // Note that both should be sorted already, and we know they are the same length. |
||||
769 | $i = 0; |
||||
770 | foreach ($possibleLengths as $length) { |
||||
771 | if ($length != $descPossibleLength[$i]) { |
||||
772 | return false; |
||||
773 | } |
||||
774 | $i++; |
||||
775 | } |
||||
776 | return true; |
||||
777 | } |
||||
778 | |||||
779 | /** |
||||
780 | * @param PhoneMetadata[] $metadataCollection |
||||
781 | * @return array |
||||
782 | */ |
||||
783 | public static function buildCountryCodeToRegionCodeMap($metadataCollection) |
||||
784 | { |
||||
785 | $countryCodeToRegionCodeMap = array(); |
||||
786 | |||||
787 | foreach ($metadataCollection as $metadata) { |
||||
788 | $regionCode = $metadata->getId(); |
||||
789 | $countryCode = $metadata->getCountryCode(); |
||||
790 | if (\array_key_exists($countryCode, $countryCodeToRegionCodeMap)) { |
||||
791 | if ($metadata->getMainCountryForCode()) { |
||||
792 | \array_unshift($countryCodeToRegionCodeMap[$countryCode], $regionCode); |
||||
793 | } else { |
||||
794 | $countryCodeToRegionCodeMap[$countryCode][] = $regionCode; |
||||
795 | } |
||||
796 | } else { |
||||
797 | // For most countries, there will be only one region code for the country calling code. |
||||
798 | $listWithRegionCode = array(); |
||||
799 | if ($regionCode != '') { // For alternate formats, there are no region codes at all. |
||||
800 | $listWithRegionCode[] = $regionCode; |
||||
801 | } |
||||
802 | $countryCodeToRegionCodeMap[$countryCode] = $listWithRegionCode; |
||||
803 | } |
||||
804 | } |
||||
805 | |||||
806 | return $countryCodeToRegionCodeMap; |
||||
807 | } |
||||
808 | } |
||||
809 |