File Coverage

blib/lib/URI/tel.pm
Criterion Covered Total %
statement 270 396 68.1
branch 127 206 61.6
condition 39 77 50.6
subroutine 29 44 65.9
pod 18 30 60.0
total 483 753 64.1


line stmt bran cond sub pod time code
1             ##----------------------------------------------------------------------------
2             ## URI::tel - ~/lib/URI/tel.pm
3             ## Version v0.801.1
4             ## Copyright(c) 2021 DEGUEST Pte. Ltd.
5             ## Author: Jacques Deguest
6             ## Created 2016/02/12
7             ## Modified 2021/08/18
8             ## All rights reserved
9             ##
10             ## This program is free software; you can redistribute it and/or modify it
11             ## under the same terms as Perl itself.
12             ##----------------------------------------------------------------------------
13             package URI::tel;
14             BEGIN
15 0         0 {
16 3     3   156056 use strict;
  3         7  
  3         146  
17 3     3   1540 use parent 'URI';
  3         1205  
  3         20  
18 3     3   285 our( $VERSION, $VERBOSE, $DEBUG, $ERROR );
19 3         4 our( $RESERVED, $MARK, $UNRESERVED, $PCT_ENCODED, $URIC, $ALPHA, $DIGIT, $ALPHANUM, $HEXDIG );
20 3         4 our( $PARAM_UNRESERVED, $VISUAL_SEPARATOR, $PHONEDIGIT, $GLOBAL_NUMBER_DIGITS, $PARAMCHAR, $DOMAINLABEL, $TOPLABEL, $DOMAINNAME, $DESCRIPTOR, $PNAME, $PVALUE, $PARAMETER, $EXTENSION, $ISDN_SUBADDRESS, $CONTEXT, $PAR, $PHONEDIGIT_HEX, $GLOBAL_NUMBER, $LOCAL_NUMBER, $OTHER, $TEL_SUBSCRIBER, $TEL_URI, $VANITY, $VANITY_LOCAL );
21 3         5 our( $COUNTRIES, $IDD_RE );
22 3         15523 $VERSION = 'v0.801.1';
23             use overload ('""' => 'as_string',
24 0     0   0 '==' => sub { _obj_eq(@_) },
25 0     0   0 '!=' => sub { !_obj_eq(@_) },
26 3         40 fallback => 1,
27 3     3   41469 );
  3         5  
28             };
29              
30             {
31             #$RESERVED = qr{[\[\;\/\?\:\@\&\=\+\$\,\[\]]+};
32             $RESERVED = q{[;/?:@&=+$,[]+};
33             $MARK = q{-_.!~*'()}; #'; emacs
34             $UNRESERVED = qq{A-Za-z0-9\Q$MARK\E};
35             ## "%" HEXDIG HEXDIG
36             $PCT_ENCODED = qr{\%[0-9A-Fa-f]{2}};
37             #$URIC = quotemeta( $RESERVED ) . $UNRESERVED . "%";
38             $URIC = qr{(?:[\Q$RESERVED\E]+|[$UNRESERVED]+|(?:$PCT_ENCODED)+)};
39             # $ALPHA = qr{A-Za-z};
40             # $DIGIT = qr{\d};
41             $ALPHA = qr{A-Za-z};
42             $DIGIT = qq{0-9};
43             # $ALPHANUM = qr{A-Za-z\d};
44             $ALPHANUM = qq{A-Za-z0-9};
45             # $HEXDIG = qr{\dA-F};
46             $HEXDIG = qr{\dA-F};
47             # $PARAM_UNRESERVED = qr{[\[\]\/\:\&\+\$]+};
48             $PARAM_UNRESERVED = q{[]/:&+$};
49             $VISUAL_SEPARATOR = q(-.());
50             ## DIGIT / [ visual-separator ]
51             # $PHONEDIGIT = qr{$DIGIT\Q$VISUAL_SEPARATOR\E};
52             $PHONEDIGIT = qq{$DIGIT\Q$VISUAL_SEPARATOR\E};
53             ## "+" *phonedigit DIGIT *phonedigit
54             $GLOBAL_NUMBER_DIGITS = qr{\+[$PHONEDIGIT]*[$DIGIT]+[$PHONEDIGIT]*};
55             ## param-unreserved / unreserved / pct-encoded
56             $PARAMCHAR = qr{(?:[\Q$PARAM_UNRESERVED\E]+|[$UNRESERVED]+|(?:$PCT_ENCODED)+)};
57             ## alphanum / alphanum *( alphanum / "-" ) alphanum
58             $DOMAINLABEL = qr{(?:[$ALPHANUM]+|[$ALPHANUM]+(?:[$ALPHANUM\-]+)*[$ALPHANUM]+)};
59             ## ALPHA / ALPHA *( alphanum / "-" ) alphanum
60             $TOPLABEL = qr{(?:[$ALPHA]+|[$ALPHA]+[$ALPHANUM\-]*[$ALPHANUM]+)};
61             ## *( domainlabel "." ) toplabel [ "." ]
62             $DOMAINNAME = qr{(?:$DOMAINLABEL\.)*(?:$TOPLABEL\.?)+};
63             ## domainname / global-number-digits
64             $DESCRIPTOR = qr{(?:$DOMAINNAME|$GLOBAL_NUMBER_DIGITS)};
65             ## 1*( alphanum / "-" )
66             $PNAME = qr{[$ALPHANUM\-]+};
67             ## 1*paramchar
68             $PVALUE = qr{(?:$PARAMCHAR)};
69             ## ";" pname ["=" pvalue ]
70             $PARAMETER = qr{\;$PNAME=$PVALUE};
71             ## ";ext=" 1*phonedigit
72             ##$EXTENSION = qr{\;ext=[$PHONEDIGIT]+}msxi;
73             ## Tweaking the rfc regular expression to add often used extension format
74             ## See: https://discussions.apple.com/thread/1635858?start=0&tstart=0
75             ## or
76             ## http://stackoverflow.com/questions/2403767/string-format-phone-numbers-with-extension
77             $EXTENSION = qr{(?:[\;\,]?ext[\=\.]?|x)[$PHONEDIGIT]+}msxi;
78             ## ";isub=" 1*uric
79             $ISDN_SUBADDRESS = qr{\;isub=$URIC}msxi;
80             ## ";phone-context=" descriptor
81             $CONTEXT = qr{\;phone-context=$DESCRIPTOR}msxi;
82             ## parameter / extension / isdn-subaddress
83             $PAR = qr{(?:(?:($PARAMETER)|($EXTENSION)|($ISDN_SUBADDRESS)))+};
84             ## HEXDIG / "*" / "#" / [ visual-separator ]
85             $PHONEDIGIT_HEX = qr{[$HEXDIG\*\#$VISUAL_SEPARATOR]+};
86             ## *phonedigit-hex (HEXDIG / "*" / "#")*phonedigit-hex
87             $LOCAL_NUMBER_DIGITS = qr{(?:$PHONEDIGIT_HEX)?[$HEXDIG\*\#]+(?:$PHONEDIGIT_HEX)?};
88             ## global-number-digits *par => "+" *phonedigit DIGIT *phonedigit
89             $GLOBAL_NUMBER = qr{($GLOBAL_NUMBER_DIGITS)($PAR)*};
90             ## local-number-digits *par context *par
91             $LOCAL_NUMBER = qr{($LOCAL_NUMBER_DIGITS)($PAR)*($CONTEXT)($PAR)*};
92             ## This is a non-rfc standard requirement, but a necessity to catch local number with no context
93             ## such as 03-1234-5678 plain and simple
94             $OTHER = qr{(\+?[$PHONEDIGIT]*[$DIGIT]+[$PHONEDIGIT]*)($PAR)*};
95             ## Like +1-800-LAWYR-UP => +1-800-52997-87
96             $VANITY_LOCAL = qr{[A-Z0-9\Q$VISUAL_SEPARATOR\E]+[$PHONEDIGIT]*};
97             $VANITY = qr{(\+?[$PHONEDIGIT]*$VANITY_LOCAL)};
98             $TEL_SUBSCRIBER = qr{(?:$VANITY|$GLOBAL_NUMBER|$LOCAL_NUMBER|$OTHER)}xs;
99             ##$TEL_SUBSCRIBER = qr{(?:$GLOBAL_NUMBER)};
100             $TEL_URI = qr{(?:tel\:)?$TEL_SUBSCRIBER};
101             # https://tools.ietf.org/search/rfc3966#section-3
102            
103             $COUNTRIES = {};
104             }
105              
106             sub _init
107             {
108 0     0   0 my $class = shift( @_ );
109 0         0 my( $str, $scheme ) = @_;
110             # find all funny characters and encode the bytes.
111 0         0 $str = $class->_uric_escape( $str );
112 0 0 0     0 $str = "$scheme:$str" unless $str =~ /^[a-zA-Z][a-zA-Z0-9.+\-]*:/o ||
113             $class->_no_scheme_ok;
114 0         0 return( $class->new( $str ) );
115             }
116              
117             sub new
118             {
119 290     290 1 680242 my $this = shift( @_ );
120 290         709 my $str = shift( @_ );
121 290   66     3014 my $class = ref( $this ) || $this;
122 290         539 my $orig = $str;
123 290         1061 $str =~ s/[[:blank:]]+//gs;
124 290         581 my $temp = {};
125 290         512 my @matches = ();
126 290         639 my @names = ();
127 290 100       8873 if( @matches = $str =~ /^((?:tel\:)?$GLOBAL_NUMBER)$/ )
    100          
    100          
    100          
128             {
129 262         5710 @names = qw( all subscriber params last_param );
130 262         10506 $temp->{type} = 'global';
131             }
132             elsif( @matches = $str =~ /^((?:tel\:)?$LOCAL_NUMBER)$/ )
133             {
134 5         29 $temp->{type} = 'local';
135 5         31 @names = qw( all subscriber params1 last_param1 ignore5 ignore6 context params2 last_param2 ignore10 ignore11 );
136 5 50       34 $temp->{_has_context_param} = 1 if( length( $matches[6] ) );
137 5         13 $temp->{local_number} = $str;
138 5         29 $temp->{local_number} =~ s/^tel\://;
139             }
140             ## e.g. 911, 110 or just ordinary local phones like 03-1234-5678
141             elsif( @matches = $str =~ /^((?:tel\:)?$OTHER)$/ )
142             {
143 19         98 $temp->{type} = 'other';
144 19         98 @names = qw( all subscriber params ignore4 last_param );
145 19         56 $temp->{local_number} = $matches[1];
146             }
147             ## e.g. +1-800-LAWYR-UP
148             elsif( @matches = $str =~ /^((?:tel\:)?$VANITY)$/ )
149             {
150 2         12 $temp->{type} = 'vanity';
151 2         9 @names = qw( all subscriber );
152 2         17 my $prefix = $this->_extract_intl_code( $matches[0] );
153 2 100       9 if( $prefix )
154             {
155 1         7 $temp->{local_number} = $this->_extract_local_number( "\+$prefix", $matches[1] );
156             }
157             else
158             {
159 1         4 $temp->{local_number} = $matches[1];
160             }
161             }
162             else
163             {
164 2         9 $ERROR = "Unknown telephone number '$str'.";
165 2         83 warn( $ERROR );
166             }
167            
168             ## The component name for each match
169 290         1676 @$temp{ @names } = @matches;
170            
171 290 100       1785 $temp->{params} = $temp->{params1} ? $temp->{params1} : $temp->{params2} if( !length( $temp->{params} ) );
    100          
172 290         1056 $temp->{context} =~ s/;[^=]+=(.*?)$/$1/gs;
173            
174 290 100 100     4362 if( $str =~ /^(?:tel\:)?\+/ )
    100 66        
175             {
176 263         1005 $temp->{intl_code} = $this->_extract_intl_code( $str );
177             }
178             elsif( $temp->{context} &&
179             $temp->{context} !~ /^[a-zA-Z]/ &&
180             substr( $temp->{context}, 0, 1 ) eq '+' )
181             {
182 2         78 $temp->{intl_code} = $this->_extract_intl_code( $temp->{context} );
183             ## We flag it as extracted from context, because we do not want to prepend the subscriber number with it.
184             ## It's just too dangerous as we cannot tell the subscriber number is actually a proper number that can be dialed from outside e.g. 911 or 110 are emergency number who may heva a context with international code
185             ## However, if the international code was provided by the user then that's his responsibility
186             ## If the user wants to just provide some context, then better to use context() instead.
187             ## Knowing the international code helps getting some other useful information, but it should not necessarily affect the format of the number
188 2         9 $temp->{_intl_code_from_context} = 1;
189             }
190 290 100 100     1898 $temp->{context} = '+' . $temp->{intl_code} if( !length( $temp->{context} ) && length( $temp->{intl_code} ) );
191            
192 290 50 66     1430 if( $temp->{type} eq 'global' && $temp->{intl_code} )
193             {
194 262         917 $temp->{local_number} = $this->_extract_local_number( $temp->{intl_code}, $temp->{subscriber} );
195             }
196            
197             my $hash = {
198             'original' => ( $orig ne $str ) ? $orig : $temp->{all},
199             'is_global' => $temp->{type} eq 'global' ? 1 : 0,
200             'is_local' => ( $temp->{type} eq 'local' or $temp->{type} eq 'other' ) ? 1 : 0,
201             'is_other' => $temp->{type} eq 'other' ? 1 : 0,
202             'is_vanity' => $temp->{type} eq 'vanity' ? 1 : 0,
203             'subscriber' => $temp->{subscriber},
204             'params' => $temp->{params},
205             'last_param' => $temp->{last_param} ? $temp->{last_param} : $temp->{last_param1} ? $temp->{last_param1} : $temp->{last_param2},
206             'context' => $temp->{context},
207             'intl_code' => $temp->{intl_code},
208             '_intl_code_from_context' => $temp->{_intl_code_from_context},
209             'local_number' => $temp->{local_number},
210 290 100 100     6942 };
    100          
    100          
    100          
    100          
    100          
    100          
211 290         900 my $self = bless( $hash, $class );
212 290         560 my $prams = [];
213 290 100       831 if( length( $temp->{params} ) )
214             {
215 10         45 my $pram_str = $temp->{params};
216 10         53 $pram_str =~ s/^[\.\,\#\;]//;
217 10         66 $prams = [ $self->split_str( $pram_str ) ];
218             }
219             ## Private parameters
220 290         537 my $priv = {};
221 290         690 foreach my $this ( @$prams )
222             {
223 16         309 $this =~ s/^(x|ext)\.?(\d+)$/ext=$2/i;
224 16         91 my( $p, $v ) = split( /=/, $this, 2 );
225 16         48 $p =~ s/\-/\_/gs;
226 16 100       110 if( lc( $p ) =~ /^(ext|isdn_subaddress)$/ )
    100          
227             {
228 10         46 $hash->{ lc( $p ) } = $v;
229             }
230             elsif( lc( $p ) eq 'phone_context' )
231             {
232 2         6 $hash->{context} = $v;
233 2         9 $temp->{_has_context_param} = 1;
234             }
235             else
236             {
237 4         17 $priv->{ lc( $p ) } = $v;
238             }
239             }
240 290         817 $self->{private} = $priv;
241 290 100 100     1668 $self->{ext} = $temp->{ext} if( !length( $hash->{ext} ) && !$self->{is_vanity} );
242 290         852 $self->{ext} =~ s/\D//gs;
243             ## Because a context may be +81 or it could be a domain name example.com
244             ## if we had it as a parameter at instantiation, we remember it and honour it when we stringify
245 290 100       783 $self->{_prepend_context} = $temp->{_has_context_param} ? 0 : 1;
246 290         2201 return( $self );
247             }
248              
249             sub as_string
250             {
251 49     49 1 35811 my $self = shift( @_ );
252 49 100       264 return( $self->{cache} ) if( length( $self->{cache} ) );
253 47 100       382 my @uri = ( 'tel:' . $self->{subscriber} ) if( length( $self->{subscriber} ) );
254 47         101 my @params = ();
255 47 100       197 push( @params, sprintf( "ext=%s", $self->{ext} ) ) if( length( $self->{ext} ) );
256 47 50       165 push( @params, sprintf( "isub=%s", $self->{isdn_subaddress} ) ) if( length( $self->{isdn_subaddress} ) );
257 47 100       159 if( length( $self->{context} ) )
258             {
259 31 100 66     230 if( !$self->{_prepend_context} )
    100          
260             {
261 15         70 push( @params, sprintf( "phone-context=%s", $self->{context} ) );
262             }
263             ## Context is not some domain name
264             elsif( $self->{subscriber} !~ /^\+\d+/ && $self->{context} !~ /[a-zA-Z]+/ )
265             {
266 1         7 @uri = ( 'tel:' . $self->{context} . '-' . $self->{subscriber} );
267             }
268             }
269 47 100 100     243 if( length( $self->{intl_code} ) && !$self->{_intl_code_from_context} )
270             {
271 19 0 33     100 if( $self->{subscriber} !~ /^\+\d+/ && $uri[0] !~ /^tel\:\+/ )
272             {
273 0         0 @uri = ( 'tel:' . '+' . $self->{intl_code} . '-' . $self->{subscriber} );
274             }
275             }
276 47         108 my $priv = $self->{private};
277 47         234 foreach my $k ( sort( keys( %$priv ) ) )
278             {
279 8 50       51 push( @params, sprintf( "$k=%s", $priv->{ $k } ) ) if( length( $priv->{ $k } ) );
280             }
281 47 100       189 push( @uri, join( ';', @params ) ) if( scalar( @params ) );
282 47         194 $self->{cache} = join( ';', @uri );
283 47         359 return( $self->{cache} );
284             }
285              
286 0     0 0 0 sub letters2digits { return( shift->aton( @_ ) ); }
287              
288             sub aton
289             {
290 21     21 1 57 my $self = shift( @_ );
291 21   66     198 my $str = shift( @_ ) || $self->{subscriber};
292 21         85 my $letters = 'abcdefghijklmnopqrstuvwxyz';
293 21         73 my $digits = '22233344455566677778889999';
294 21 100 66     218 return( $str ) if( $str !~ /[a-zA-Z]+/ || !$self->is_vanity );
295 2         12 $str = lc( $str );
296 2         5 my $res = '';
297 2         12 for( my $i = 0; $i < length( $str ); $i++ )
298             {
299 26         46 my $c = substr( $str, $i, 1 );
300 26         44 my $p = index( $letters, $c );
301 26 100       77 $res .= $p != -1 ? substr( $digits, $p, 1 ) : $c;
302             }
303 2         7 return( $res );
304             }
305              
306             sub canonical
307             {
308 21     21 1 2541 my $self = shift( @_ );
309 21         107 my $tel = $self->aton;
310 21         364 $tel =~ s/[\Q$VISUAL_SEPARATOR\E]+//gs;
311 21         140 my $uri = $self->new( "tel:$tel" );
312 21 100       144 $uri->ext( $self->{ext} ) if( length( $self->{ext} ) );
313 21 50       197 $uri->isub( $self->{isdn_subaddress} ) if( length( $self->{isdn_subaddress} ) );
314 21 100       139 $uri->context( $self->{context} ) if( length( $self->{context} ) );
315 21 100       124 $uri->international_code( $self->{intl_code} ) if( length( $self->{intl_code} ) );
316 21         63 $uri->{_has_context_param} = $self->{_has_context_param};
317 21         55 $uri->{_prepend_context} = $self->{_prepend_context};
318 21         56 $uri->{_intl_code_from_context} = $self->{_intl_code_from_context};
319 21         58 $uri->{local_number} = $self->{local_number};
320 21         45 my $priv = $self->{private};
321 21         65 %{$uri->{private}} = %$priv;
  21         71  
322 21         204 return( $uri );
323             }
324              
325             sub cc2context
326             {
327 1     1 0 10 my $self = shift( @_ );
328 1         5 my $cc = uc( shift( @_ ) );
329 1 50       6 return( $self->error( "No country code provided." ) ) if( !length( $cc ) );
330 1         6 $self->_load_countries;
331 1         3 my $hash = $COUNTRIES;
332 1         299 foreach my $k ( sort( keys( %$hash ) ) )
333             {
334             ## array ref
335 205         441 my $ref = $hash->{ $k };
336 205         362 foreach my $this ( @$ref )
337             {
338 217 100       673 if( $this->{cc} eq $cc )
339             {
340 1         12 return( '+' . $k );
341             }
342             }
343             }
344             ## Nothing found
345 0         0 return( '' );
346             }
347              
348             sub clone
349             {
350 0     0 1 0 my $self = shift( @_ );
351 0         0 my $class = ref( $self );
352 0         0 my $hash = {};
353 0         0 my @keys = keys( %$self );
354 0         0 @$hash{ @keys } = @$self{ @keys };
355 0         0 delete( $hash->{cache} );
356 0         0 return( bless( $hash, $class ) );
357             }
358              
359             sub context
360             {
361 280     280 1 2507 my $self = shift( @_ );
362 280 100       765 if( @_ )
363             {
364 14         40 my $str = shift( @_ );
365 14 50       482 if( $str !~ /^$DESCRIPTOR$/ )
366             {
367 0         0 warn( "'$str' is not a valid context\n" );
368 0         0 return( undef() );
369             }
370 14         45 delete( $self->{cache} );
371 14         56 $self->{_has_context_param} = 1;
372 14         62 $self->{_prepend_context} = 0;
373 14         42 $self->{context} = $str;
374             ## We found an international country code in the prefix provided, so let's set it
375 14 100 100     98 if( !length( $self->{intl_code} ) && ( my $code = $self->_extract_intl_code( $str ) ) )
376             {
377 3         13 $self->{_intl_code_from_context} = 1;
378 3         11 $self->{intl_code} = $code;
379             }
380             }
381 280         1071 return( $self->{context} );
382             }
383              
384             sub country
385             {
386 266     266 1 624 my $self = shift( @_ );
387 3     3   49 no overloading;
  3         6  
  3         9047  
388 266         948 $self->_load_countries;
389 266         411 my $hash = $COUNTRIES;
390 266         912 my $code = $self->international_code;
391 266 50       867 return( wantarray() ? () : [] ) if( !length( $code ) );
    100          
392 255         2244 $code =~ s/[\Q$VISUAL_SEPARATOR\E]+//g;
393 255 50       2480 if( $code =~ /^\+($IDD_RE)/ )
394             {
395 0         0 $code = $1;
396             }
397 255 0       3059 return( wantarray() ? () : [] ) if( !exists( $hash->{ $code } ) );
    50          
398 255         585 my $ref = $hash->{ $code };
399 255 50       957 return( wantarray() ? @$ref : \@$ref );
400             }
401              
402             sub country_code
403             {
404 245     245 0 299711 my $self = shift( @_ );
405 245         861 my $ref = $self->country_codes;
406 245 50       568 return( '' ) if( ref( $ref ) ne 'ARRAY' );
407 245         2361 return( $ref->[0] );
408             }
409              
410             sub country_codes
411             {
412 245     245 0 402 my $self = shift( @_ );
413 245         665 my $ref = $self->country;
414 245 50       1956 return( '' ) if( ref( $ref ) ne 'ARRAY' );
415 245         1284 my @codes = map( $_->{cc}, @$ref );
416 245 50       673 return( wantarray() ? @codes : \@codes );
417             }
418              
419             sub country_name
420             {
421 0     0 0 0 my $self = shift( @_ );
422 0         0 my $ref = $self->country_names;
423 0 0       0 return( '' ) if( ref( $ref ) ne 'ARRAY' );
424 0         0 return( $ref->[0] );
425             }
426              
427             sub country_names
428             {
429 0     0 0 0 my $self = shift( @_ );
430 0         0 my $ref = $self->country;
431 0 0       0 return( '' ) if( ref( $ref ) ne 'ARRAY' );
432 0         0 my @names = map( $_->{name}, @$ref );
433 0 0       0 return( wantarray() ? @names : \@names );
434             }
435              
436             sub error
437             {
438 0     0 0 0 my $self = shift( @_ );
439 0         0 my $level = 0;
440 0         0 my $caller = caller;
441 0         0 my $err = join( '', @_ );
442 0   0     0 my $class = ref( $self ) || $self;
443 0 0 0     0 if( $err && length( $err ) )
444             {
445 0         0 my( $frame, $caller ) = ( $level, '' );
446 0         0 while( $caller = ( caller( $frame ) )[ 0 ] )
447             {
448 0 0       0 last if( $caller ne 'URI::tel' );
449 0         0 $frame++;
450             }
451 0         0 my( $pack, $file, $line ) = caller( $frame );
452 0         0 $err =~ s/\n$//gs;
453 0         0 $self->{error} = ${ $class . '::ERROR' } = $err;
  0         0  
454 0         0 return( undef() );
455             }
456 0   0     0 return( $self->{error} || $ERROR );
457             }
458              
459 0     0 0 0 sub extension { return( shift->ext( @_ ) ); }
460              
461             sub ext
462             {
463 33     33 0 526 my $self = shift( @_ );
464 33 100       139 if( @_ )
465             {
466 11         35 my $val = shift( @_ );
467 11 50 33     218 if( length( $val ) && $val !~ /^[$PHONEDIGIT]+$/ )
468             {
469 0         0 warn( "'$val' is not a valid extension ([$PHONEDIGIT]+)\n" );
470 0         0 return( undef() );
471             }
472 11         35 delete( $self->{cache} );
473 11         35 $self->{ext} = $val;
474             }
475 33         306 return( $self->{ext} );
476             }
477              
478             sub international_code
479             {
480 276     276 1 591 my $self = shift( @_ );
481 276 100       765 if( @_ )
482             {
483 10         26 my $val = shift( @_ );
484 10 50 33     157 if( length( $val ) && $val !~ /^[$PHONEDIGIT]+$/ )
485             {
486 0         0 warn( "'$val' is not a valid international code.\n" );
487 0         0 return;
488             }
489 10         26 delete( $self->{cache} );
490             ## The international code was provided by the user as opposed to using the context() method,
491             ## so we flag it properly so it can be used in stringification of the phone number
492 10         32 $self->{_intl_code_from_context} = 0;
493 10         23 $self->{is_global} = 1;
494 10         30 $self->{intl_code} = $val;
495             }
496 276         913 return( $self->{intl_code} );
497             }
498              
499             sub is_global
500             {
501 21     21 1 23783 return( shift->{is_global} );
502             }
503              
504             sub is_local
505             {
506 0     0 1 0 return( shift->{is_local} );
507             }
508              
509             sub is_other
510             {
511 0     0 1 0 return( shift->{is_other} );
512             }
513              
514             sub is_vanity
515             {
516 2     2 1 14 return( shift->{is_vanity} );
517             }
518              
519 0     0 0 0 sub isdn_subaddress { return( shift->isub( @_ ) ); }
520              
521             sub isub
522             {
523 21     21 1 66 my $self = shift( @_ );
524 21 50       111 if( @_ )
525             {
526 0         0 my $val = shift( @_ );
527 0 0 0     0 if( length( $val ) && $val !~ /^$URIC$/ )
528             {
529 0         0 warn( "'$val' is not a isdn subaddress\n" );
530 0         0 return( undef() );
531             }
532 0         0 delete( $self->{cache} );
533 0         0 $self->{isub} = $val;
534             }
535 21         215 return( $self->{isub} );
536             }
537              
538             sub local_number
539             {
540 21     21 1 67 my $self = shift( @_ );
541 21 50       113 if( @_ )
542             {
543 0         0 my $val = shift( @_ );
544 0         0 $self->{local_number} = $val;
545 0 0       0 if( length( $val ) )
546             {
547 0         0 my @matches = ();
548 0 0       0 if( @matches = $val !~ /^((?:tel\:)?$LOCAL_NUMBER)$/ )
549             {
550 0         0 $val =~ s/^tel\://;
551 0         0 @names = qw( all subscriber params1 last_param1 ignore5 ignore6 context params2 last_param2 ignore10 ignore11 );
552 0         0 my $ref = {};
553 0         0 @$ref{ @names } = @matches;
554 0 0       0 if( length( $ref->{context} ) )
555             {
556 0         0 $ref->{context} =~ s/;[^=]+=(.*?)$/$1/gs;
557 0         0 $self->{context} = $ref->{context};
558 0         0 $self->{_has_context_param} = 1;
559            
560 0 0 0     0 if( $ref->{context} !~ /^[a-zA-Z]/ &&
561             substr( $ref->{context}, 0, 1 ) eq '+' )
562             {
563 0         0 $self->{intl_code} = $self->_extract_intl_code( $ref->{context} );
564             ## We flag it as extracted from context, because we do not want to prepend the subscriber number with it.
565             ## It's just too dangerous as we cannot tell the subscriber number is actually a proper number that can be dialed from outside e.g. 911 or 110 are emergency number who may heva a context with international code
566             ## However, if the international code was provided by the user then that's his responsibility
567             ## If the user wants to just provide some context, then better to use context() instead.
568             ## Knowing the international code helps getting some other useful information, but it should not necessarily affect the format of the number
569 0         0 $self->{_intl_code_from_context} = 1;
570             }
571             }
572             }
573             else
574             {
575 0         0 warn( "'$val' is not a valid local number.\n" );
576 0         0 return;
577             }
578             }
579             }
580 21         192 return( $self->{local_number} );
581             }
582              
583             sub original
584             {
585 0     0 1 0 return( shift->{original} );
586             }
587              
588             sub prepend_context
589             {
590 1     1 0 418 my $self = shift( @_ );
591 1 50       6 if( @_ )
592             {
593 1         4 $self->{_prepend_context} = shift( @_ );
594 1         5 delete( $self->{cache} );
595             }
596 1         3 return( $self->{_prepend_context} );
597             }
598              
599             sub private
600             {
601 3     3 1 6 my $self = shift( @_ );
602 3         29 my( $name, $val ) = @_;
603 3 50       16 if( length( $name ) )
604             {
605             ## The value could be blank and if so, we would remove the parameter
606 0 0       0 if( defined( $val ) )
607             {
608 0 0       0 if( $name !~ /^$PNAME$/ )
609             {
610 0         0 warn( "'$name' is not a valid parameter name.\n" );
611 0         0 return( undef() );
612             }
613 0 0       0 if( length( $val ) )
614             {
615 0 0       0 if( $val !~ /^$PVALUE$/ )
616             {
617 0         0 warn( "'$val' is not a valid parameter value.\n" );
618 0         0 return( undef() );
619             }
620 0         0 delete( $self->{cache} );
621 0         0 $self->{private}->{ $name } = $val;
622 0         0 return( $self->{private}->{ $name } );
623             }
624             else
625             {
626 0         0 return( delete( $self->{private}->{ $name } ) );
627             }
628             }
629             else
630             {
631 0 0       0 return( wantarray() ? %${$self->{private}->{ $name }} : \%${$self->{private}->{ $name }} );
  0         0  
  0         0  
632             }
633             }
634 3 50       22 return( wantarray() ? %{$self->{private}} : \%{$self->{private}} );
  0         0  
  3         14  
635             }
636              
637             sub subscriber
638             {
639 21     21 1 70 my $self = shift( @_ );
640 21 50       171 if( @_ )
641             {
642 0         0 my $val = shift( @_ );
643 0 0 0     0 if( length( $val ) && $val !~ /^$TEL_SUBSCRIBER$/ )
644             {
645 0         0 warn( "'$val' is not a valid subscriber value.\n" );
646 0         0 return( undef() );
647             }
648 0         0 delete( $self->{cache} );
649 0         0 $self->{subscriber} = $val;
650             }
651 21         200 return( $self->{subscriber} );
652             }
653              
654             sub type
655             {
656 0     0 1 0 return( shift->{type} );
657             }
658              
659             sub _extract_intl_code
660             {
661 273     273   576 my $self = shift( @_ );
662 273   50     923 my $str = shift( @_ ) || return( '' );
663 273         1046 my $code;
664             ## Extract the global idd
665 273         1076 $self->_load_countries;
666 273         2945 ( my $str2 = $str ) =~ s/[\Q$VISUAL_SEPARATOR\E]+//g;
667 273         5268 $str2 =~ s/^tel\://;
668 273 100       3593 if( $str2 =~ /^\+($IDD_RE)/ )
669             {
670 269         904 my $idd = $1;
671 269 50       994 if( CORE::exists( $COUNTRIES->{ $idd } ) )
672             {
673 269         1014 my $idds = $COUNTRIES->{ $idd }->[0]->{idd};
674 269         830 foreach my $thisIdd ( @$idds )
675             {
676 278         573 my $check = $thisIdd;
677 278         843 $check =~ s/\D//g;
678 278 100       968 if( $check eq $idd )
679             {
680             #$temp->{context} = '+' . $thisIdd;
681 269         5564 $code = $thisIdd;
682 269         4778 last;
683             }
684             }
685             }
686             }
687 273         1228 return( $code );
688             }
689              
690             sub _extract_local_number
691             {
692 263     263   469 my $self = shift( @_ );
693 263         2062 my( $intl_code, $subscriber ) = @_;
694 263         431 my $j = 0;
695 263         984 for( my $i = 0; $i < length( $intl_code ); $i++ )
696             {
697             # Skip until we are on a number
698 802 100       2832 next if( substr( $intl_code, $i, 1 ) !~ /^\d$/ );
699             # and here too so we can compare number to number
700 759         2891 while( substr( $subscriber, $j, 1 ) !~ /^\d$/ )
701             {
702 263         808 $j++;
703             }
704             # Our international code does not seem to match the prefix of our subscriber! This should not happen.
705 759 50       2040 if( substr( $subscriber, $j, 1 ) ne substr( $intl_code, $i, 1 ) )
706             {
707             # printf( STDERR "Mismatch... %s at position %d vs %s at position %d\n", substr( $intl_code, $i, 1 ), $i, substr( $subscriber, $j, 1 ), $j );
708             #last;
709 0         0 return( '' );
710             }
711 759         1535 $j++;
712             }
713 263   66     10992 $j++ while( $j <= length( $subscriber ) && substr( $subscriber, $j ) !~ /^[()]?\d+($LOCAL_NUMBER|$OTHER|$VANITY_LOCAL)$/ );
714 263         1659 return( substr( $subscriber, $j ) );
715             }
716              
717             # Check if two objects are the same object
718             # https://tools.ietf.org/search/rfc3966#section-4
719             sub _obj_eq
720             {
721             ##return overload::StrVal( $_[0] ) eq overload::StrVal( $_[1] );
722 3     3   32 no overloading;
  3         4  
  3         862  
723 0     0   0 my $self = shift( @_ );
724 0         0 my $other = shift( @_ );
725 0 0 0     0 return( 0 ) if( !ref( $other ) || !$other->isa( 'URI::tel' ) );
726 0         0 my $sub = $self->canonical->subscriber;
727 0         0 my $sub2 = $other->canonical->subscriber;
728 0         0 $sub =~ s/^\+//;
729 0         0 $sub2 =~ s/^\+//;
730 0 0       0 return( 0 ) if( $sub ne $sub2 );
731 0         0 my $context = $self->context;
732 0         0 my $context2 = $other->context;
733 0 0       0 return( 0 ) if( $context ne $context2 );
734 0         0 my $ext = $self->ext;
735 0         0 my $ext2 = $other->ext;
736 0 0       0 return( 0 ) if( $ext ne $ext2 );
737 0         0 my $priv = $self->private;
738 0         0 my $priv2 = $other->private;
739 0         0 foreach my $k ( keys( %$priv ) )
740             {
741 0 0       0 return( 0 ) if( !exists( $priv2->{ $k } ) );
742 0 0       0 return( 0 ) if( $priv->{ $k } ne $priv2->{ $k } );
743             }
744 0         0 foreach my $k ( keys( %$priv2 ) )
745             {
746 0 0       0 return( 0 ) if( !exists( $priv->{ $k } ) );
747 0 0       0 return( 0 ) if( $priv2->{ $k } ne $priv->{ $k } );
748             }
749 3     3   21 use overloading;
  3         20  
  3         2147  
750 0         0 return( 1 );
751             }
752              
753             ## Taken from http://www.perlmonks.org/bare/?node_id=319761
754             ## This will do a split on a semi-colon, but being mindful if before it there is an escaped backslash
755             ## For example, this would not be skipped: something\;here
756             ## But this would be split: something\\;here resulting in something\ and here after unescaping
757             sub split_str
758             {
759 10     10 0 28 my $self = shift( @_ );
760 10         28 my $s = shift( @_ );
761 10 50       44 my $sep = @_ ? shift( @_ ) : ';';
762 10         23 my @parts = ();
763 10         26 my $i = 0;
764 10         166 foreach( split( /(\\.)|$sep/, $s ) )
765             {
766 22 100       60 defined( $_ ) ? do{ $parts[$i] .= $_ } : do{ $i++ };
  16         58  
  6         11  
767             }
768 10         51 return( @parts );
769             }
770              
771             sub _load_countries
772             {
773 541     541   304231 my $self = shift( @_ );
774 541 100       2224 if( !%$COUNTRIES )
775             {
776 2         5 my $in = 0;
777 2         7 my $hash = {};
778 2         711 my @data = ;
779 2         34 foreach ( @data )
780             {
781 960         1387 chomp;
782 960 100 100     2268 next unless( $in || /^\#{2} BEGIN DATA/ );
783 504 100       922 last if( /^\#{2} END DATA/ );
784 502         664 $in++;
785 502 100       872 next if( /^\#{2} BEGIN DATA/ );
786 500         3121 my( $cc, $cc3, $name, $idd ) = split( /[[:blank:]]*\;[[:blank:]]*/, $_ );
787 500 100       1175 my $keys = index( $idd, ',' ) != -1 ? [ split( /[[:blank:]]*\,[[:blank:]]*/, $idd ) ] : [ $idd ];
788 500         1907 my $info =
789             {
790             'cc' => $cc,
791             'cc3' => $cc3,
792             'name' => $name,
793             'idd' => $idd,
794             };
795 500         1005 $info->{idd} = $keys;
796 500         700 foreach my $k ( @$keys )
797             {
798 514         715 my $k2 = $k;
799 514         750 $k2 =~ s/-//gs;
800 514 100       1631 $hash->{ $k2 } = [] if( !exists( $hash->{ $k2 } ) );
801 514         643 push( @{$hash->{ $k2 }}, $info );
  514         1378  
802             }
803             }
804 2         9 $COUNTRIES = $hash;
805 2         155 my @list = sort{ $b <=> $a } keys( %$hash );
  3297         4279  
806 2         87 my $re_list = join( '|', @list );
807 2         1474 $IDD_RE = qr{(?:$re_list)};
808             }
809             }
810              
811             1;
812              
813             __DATA__