File Coverage

blib/lib/Parse/HTTP/UserAgent.pm
Criterion Covered Total %
statement 177 206 85.9
branch 110 134 82.0
condition 42 62 67.7
subroutine 19 21 90.4
pod 3 3 100.0
total 351 426 82.3


line stmt bran cond sub pod time code
1             package Parse::HTTP::UserAgent;
2             $Parse::HTTP::UserAgent::VERSION = '0.41';
3 2     2   71364 use strict;
  2         19  
  2         48  
4 2     2   9 use warnings;
  2         4  
  2         58  
5              
6 2         584 use base qw(
7             Parse::HTTP::UserAgent::Base::IS
8             Parse::HTTP::UserAgent::Base::Parsers
9             Parse::HTTP::UserAgent::Base::Dumper
10             Parse::HTTP::UserAgent::Base::Accessors
11 2     2   10 );
  2         3  
12              
13 2         7 use overload '""', => 'name',
14             '0+', => 'version',
15             fallback => 1,
16 2     2   11 ;
  2         4  
17              
18 2     2   604 use version;
  2         2525  
  2         8  
19 2     2   130 use Carp qw( croak );
  2         3  
  2         82  
20 2     2   11 use Parse::HTTP::UserAgent::Constants qw(:all);
  2         2  
  2         440  
21              
22             BEGIN {
23 2 50   2   3827 constant->import( DEBUG => 0 ) if not defined &DEBUG;
24             }
25              
26             my %OSFIX = (
27             'WinNT4.0' => 'Windows NT 4.0',
28             'WinNT' => 'Windows NT',
29             'Windows 4.0' => 'Windows 95',
30             'Win95' => 'Windows 95',
31             'Win98' => 'Windows 98',
32             'Windows 4.10' => 'Windows 98',
33             'Win 9x 4.90' => 'Windows Me',
34             'Windows NT 5.0' => 'Windows 2000',
35             'Windows NT 5.1' => 'Windows XP',
36             'Windows XP 5.1' => 'Windows XP', # huh?
37             'Windows NT 5.2' => 'Windows Server 2003',
38             'Windows NT 6.0' => 'Windows Vista / Server 2008',
39             'Windows NT 6.1' => 'Windows 7',
40             'Windows NT 6.2' => 'Windows 8',
41             'Windows NT 6.3' => 'Windows 8.1',
42             );
43              
44             sub new {
45 562     562 1 495245 my $class = shift;
46 562   33     1196 my $ua = shift || croak 'No user agent string specified';
47 562   50     1934 my $opt = shift || {};
48 562 50       1365 croak 'Options must be a hash reference' if ref $opt ne 'HASH';
49 562         1083 my $self = [ map { undef } 0..MAXID ];
  15174         18336  
50 562         1150 bless $self, $class;
51 562         926 @{ $self }[ UA_STRING, UA_STRING_ORIGINAL ] = ($ua) x 2;
  562         1362  
52 562 50       1207 $self->[IS_EXTENDED] = exists $opt->{extended} ? $opt->{extended} : 1;
53 562 50       1325 $self->_normalize( $opt->{normalize} ) if $opt->{normalize};
54 562         1204 $self->_parse;
55 562         1423 return $self;
56             }
57              
58             sub as_hash {
59 281     281 1 829 my $self = shift;
60 281         342 my %struct;
61 281         444 foreach my $id ( $self->_object_ids ) {
62 6463         14567 (my $name = $id) =~ s{ \A UA_ }{}xms;
63 6463         15953 $struct{ lc $name } = $self->[ $self->$id() ];
64             }
65 281         3709 return %struct;
66             }
67              
68             sub trim {
69 1370     1370 1 1700 my $self = shift;
70 1370         1614 my $s = shift;
71 1370 50       2002 return $s if ! $s;
72 1370         2463 $s =~ s{ \A \s+ }{}xms;
73 1370         2235 $s =~ s{ \s+ \z }{}xms;
74 1370         2601 return $s;
75             }
76              
77             sub _normalize {
78 0     0   0 my $self = shift;
79 0         0 my $nopt = shift;
80 0         0 my $type = ref $nopt;
81              
82             my @o = ! $type ? ':all'
83 0 0       0 : $type eq 'ARRAY' ? @{ $nopt }
  0 0       0  
84             : croak "Normalization option $nopt is invalid";
85              
86 0         0 my %mode = map { $_ => 1 } @o;
  0         0  
87 0         0 my @all = qw( plus_to_space trim_spaces );
88 0 0       0 @mode{ @all } = (1) x @all if delete $mode{':all'};
89              
90 0         0 my $s = \$self->[UA_STRING];
91 0 0       0 ${$s} =~ s{[+]}{ }xmsg if $mode{plus_to_space};
  0         0  
92 0 0       0 ${$s} =~ s<\s+>< >xmsg if $mode{trim_spaces};
  0         0  
93 0         0 return;
94             }
95              
96             sub _parse {
97 562     562   719 my $self = shift;
98 562 50       971 return $self if $self->[IS_PARSED];
99 562         966 $self->_do_parse( $self->_pre_parse );
100 562         1106 $self->[IS_PARSED] = 1;
101 562 50       1601 $self->_post_parse if ! $self->[UA_UNKNOWN];
102 562         757 return;
103             }
104              
105             sub _pre_parse {
106 562     562   697 my $self = shift;
107 562         746 my $ua = $self->[UA_STRING];
108 562         1063 my $uc_ua = uc $ua;
109              
110 562         1301 $self->[IS_MAXTHON] = index($uc_ua, 'MAXTHON') != NO_IMATCH;
111 562         945 $self->[IS_TRIDENT] = index($uc_ua, 'TRIDENT/') != NO_IMATCH;
112              
113 562         745 my @parts;
114 562         634 my $i = 0;
115 562         680 my $depth = 0;
116              
117 562         4179 foreach my $token ( split RE_SPLIT_PARSE, $ua ) {
118 3064 100       4978 if ( $token eq '(' ) {
119 694 100       1170 $i++ if ++$depth == 1;
120 694         1091 next;
121             }
122 2370 100       3427 if ( $token eq ')' ) {
123 692 100       1177 $i++ if --$depth == 0;
124 692         949 next;
125             }
126 1678   100     1896 push @{ $parts[$i] ||= [] }, $token;
  1678         4942  
127             }
128              
129             # Hopefully the above code was successful and now we can set the actual
130             # tokens to use inside parsers.
131 562 50       984 my($moz) = join ' ', @{ shift(@parts) || [] };
  562         1722  
132 562 100       945 my($thing) = join ' ', @{ shift(@parts) || [] };
  562         1243  
133 562 100       831 my($extra) = join ' ', @{ shift(@parts) || [] };
  562         1405  
134 562         907 my(@others) = map { @{ $_ } } @parts;
  262         325  
  262         503  
135              
136 562 100       3014 $thing = $thing ? [ split RE_SC_WS, $thing ] : [];
137 562 100       1657 $extra = [ split RE_WHITESPACE, $extra ] if $extra;
138              
139 562         741 $self->_debug_pre_parse( $moz, $thing, $extra, @others ) if DEBUG;
140 562         2070 return $moz, $thing, $extra, @others;
141             }
142              
143             sub _do_parse {
144 562     562   1187 my($self, $m, $t, $e, @o) = @_;
145              
146 562   100     1571 my $c = $t->[0] && $t->[0] eq 'compatible';
147              
148 562 100 66     1077 if ( $c
      100        
      100        
      100        
149 212         1100 && shift @{$t} # just inline removal of "compatible"
150             && ( ! $e || $self->[IS_TRIDENT] ) # older versions don't have junk outside, while newer might have
151             && ! $self->[IS_MAXTHON] # be sure that this is not the faker
152             ) {
153 150         541 my($n, $v) = split RE_WHITESPACE, $t->[0];
154 150 100 100     486 if ( $n eq 'MSIE' && index($m, q{ }) == NO_IMATCH ) {
155 120         392 return $self->_parse_msie($m, $t, $e, $n, $v);
156             }
157             }
158              
159 442 100       833 if ( $self->[IS_TRIDENT] ) {
160             # http://blogs.msdn.com/b/ieinternals/archive/2013/09/21/internet-explorer-11-user-agent-string-ua-string-sniffing-compatibility-with-gecko-webkit.aspx
161             my %msie11 = map {
162 164 100       417 index( $_, 'Windows') != NO_IMATCH ? ( windows => 1 )
    100          
    100          
163             : index( $_, 'Trident/') != NO_IMATCH ? ( trident => 1 )
164             : index( $_, 'rv:') != NO_IMATCH ? ( version => 1 )
165             : ()
166 26         46 } @{ $t };
  26         46  
167 26         56 my $msie_matched = keys %msie11;
168              
169 26 100 66     121 if ( $msie_matched == 3 ){
    100 66        
170 12         54 return $self->_parse_msie_11($m, $t, $e);
171             }
172             elsif ( ! $self->[IS_MAXTHON] && $msie_matched == 2 && ! $msie11{version} ) {
173             # another weird case. robot?
174 2         4 my(@buf, $vstr);
175 2         4 for my $junk ( @{ $t } ) {
  2         6  
176 8 100       21 if ( index( $junk, 'MSIE') != NO_IMATCH ) {
177 2         4 $vstr = $junk;
178 2         79 next;
179             }
180 6         10 push @buf, $junk;
181             }
182              
183 2         55 my $rv = $self->_parse_msie($m, \@buf, $e, split( RE_WHITESPACE, $vstr ) );
184 2         8 return $rv;
185             }
186             # fall back to the dispatch table below
187             }
188              
189 428 100       1318 my $rv = $self->[IS_MAXTHON] ? [ maxthon => $m, $t, $e, @o ]
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
190             : $self->_is_opera_pre($m) ? [ opera_pre => $m, $t, $e ]
191             : $self->_is_opera_post($e) ? [ opera_post => $m, $t, $e, $c ]
192             : $self->_is_opera_ff($e) ? [ opera_pre => "$e->[2]/$e->[3]", $t ]
193             : $self->_is_ff($e) ? [ firefox => $m, $t, $e, @o ]
194             : $self->_is_safari($e, \@o) ? [ safari => $m, $t, $e, @o ]
195             : $self->_is_chrome($e, \@o) ? [ chrome => $m, $t, $e, @o ]
196             : $self->_is_android($t,\@o) ? [ android => $m, $t, $e, @o ]
197             : $self->_is_suspicious_ff($e) ? [ ff_suspect => $m, $t, $e, @o ]
198             : undef;
199              
200 428 100       942 if ( $rv ) {
201 242         299 my $pname = shift @{ $rv };
  242         367  
202 242         466 my $method = '_parse_' . $pname;
203 242         320 my $rvx = $self->$method( @{ $rv } );
  242         889  
204 242 100       470 if ( $rvx ) {
205 240   66     750 $self->[UA_PARSER] ||= $pname;
206 240         583 return $rvx;
207             }
208             }
209              
210 188 50       534 return $self->_extended_probe($m, $t, $e, $c, @o) if $self->[IS_EXTENDED];
211              
212 0         0 $self->[UA_UNKNOWN] = 1; # give up
213 0         0 return;
214             }
215              
216             sub _post_parse {
217 562     562   713 my $self = shift;
218 562 100       1283 $self->[UA_VERSION] = $self->_numify( $self->[UA_VERSION_RAW] )
219             if $self->[UA_VERSION_RAW];
220              
221 562         899 my @buf;
222 562         696 foreach my $e ( @{ $self->[UA_EXTRAS] } ) {
  562         1168  
223 878 100       1772 if ( $self->_is_strength( $e ) ) {
224 76         119 $self->[UA_STRENGTH] = $e ;
225 76         140 next;
226             }
227 802         1415 push @buf, $e;
228             }
229              
230 562 100       1479 $self->[UA_EXTRAS] = @buf ? [ @buf ] : undef;
231              
232 562 100       1097 if ( $self->[UA_TOOLKIT] ) {
233 266         403 my $v = $self->[UA_TOOLKIT][TK_ORIGINAL_VERSION];
234 266 100       354 push @{ $self->[UA_TOOLKIT] }, defined $v ? $self->_numify( $v ) : 0;
  266         686  
235             }
236              
237 562 100       1083 if( $self->[UA_MOZILLA] ) {
238 90         152 $self->[UA_MOZILLA] =~ tr/a-z://d;
239 90         191 $self->[UA_MOZILLA] = [ $self->[UA_MOZILLA],
240             $self->_numify( $self->[UA_MOZILLA] ) ];
241             }
242              
243 562 100       959 if ( $self->[UA_OS] ) {
244 498   66     1453 $self->[UA_OS] = $OSFIX{ $self->[UA_OS] } || $self->[UA_OS];
245             }
246              
247 562         1063 foreach my $robo ( LIST_ROBOTS ) { # regex???
248 4980 100       9558 next if lc $robo ne lc $self->[UA_NAME];
249 18         23 $self->[UA_ROBOT] = 1;
250 18         24 last;
251             }
252 562         852 return;
253             }
254              
255             sub _extended_probe {
256 188     188   454 my($self, @args) = @_;
257              
258 188 100 66     356 return if $self->_is_gecko && $self->_parse_gecko( @args );
259 138 100 66     321 return if $self->_is_netscape( @args ) && $self->_parse_netscape( @args );
260 112 100 66     251 return if $self->_is_docomo( @args ) && $self->_parse_docomo( @args );
261 110 100       224 return if $self->_is_generic( @args );
262 26 100 66     69 return if $self->_is_emacs( @args ) && $self->_parse_emacs( @args );
263 22 100 66     57 return if $self->_is_moz_only( @args ) && $self->_parse_moz_only( @args );
264 2 50 33     25 return if $self->_is_hotjava( @args ) && $self->_parse_hotjava( @args );
265              
266 0         0 $self->[UA_UNKNOWN] = 1;
267 0         0 return;
268             }
269              
270             sub _object_ids {
271 562     562   8400 return grep { $_ =~ RE_OBJECT_ID } keys %Parse::HTTP::UserAgent::;
  74532         109863  
272             }
273              
274             sub _numify {
275 934     934   1228 my $self = shift;
276 934   50     1684 my $v = shift || return 0;
277 934         1111 my @removed;
278              
279 934 100       3799 if (
280             $v =~ s{(
281             pre |
282             rel |
283             alpha |
284             beta |
285             \-stable |
286             gold |
287             [ab]\d+ |
288             a\-XXXX |
289             dev |
290             [+]
291             )}{}xmsig
292             ){
293 46         70 push @removed, $1 if INSIDE_VERBOSE_TEST;
294             }
295              
296 934 100       3157 if (
297             $v =~ s{(
298             (?:[^0-9]+)? # usually dash
299             rc # nonsense
300             [\-_.]? # usually dash
301             ([0-9]) # teh candidate revision
302             )}{.0.$2}xmsi # yeah, hacky
303             ) {
304 2         5 push @removed, $1 if INSIDE_VERBOSE_TEST;
305             }
306              
307             # workaround another stupidity (1.2.3-4)
308 934 100       1981 if ( my $rc = $v =~ tr/-/./ ) {
309 2         4 push @removed, '-' x $rc if INSIDE_VERBOSE_TEST;
310             }
311              
312             # convert _ to .
313             # version.pm has changed its interpretation of versions with underlines
314             # cf. https://bugs.debian.org/825611
315 934 100       1595 if ( my $rc = $v =~ tr/_/./ ) {
316 2         5 push @removed, '-' x $rc if INSIDE_VERBOSE_TEST;
317             }
318              
319             # Finally, be aggressive to prevent dying on bogus stuff.
320             # It's interesting how people provide highly stupid version "numbers".
321             # Version parameters are probably more stupid than the UA string itself.
322 934 100       1961 if ( $v =~ s<([^0-9._v])><.>xmsg ) {
323 2         4 push @removed, $1 if INSIDE_VERBOSE_TEST;
324             }
325              
326 934 50       1863 if ( $v =~ s<([.]{2,})><.>xmsg ) {
327 0         0 push @removed, $1 if INSIDE_VERBOSE_TEST;
328             }
329              
330 934         1079 if ( INSIDE_VERBOSE_TEST ) {
331             if ( @removed ) {
332             my $r = join q{','}, @removed;
333             require Test::More;
334             Test::More::diag("[DEBUG] _numify: removed '$r' from version string");
335             }
336             }
337              
338             # Gecko revisions like: "20080915000512" will cause an
339             # integer overflow warning. use bigint?
340             local $SIG{__WARN__} = sub {
341 2     2   5 my $msg = shift;
342 2 50 33     25 warn "$msg\n" if $msg !~ RE_WARN_OVERFLOW && $msg !~ RE_WARN_INVALID;
343 934         5051 };
344             # if version::vpp is used it'll identify 420 as a v-string
345             # add a floating point to fool it
346 934 100       2345 $v .= q{.0} if index($v, q{.}) == NO_IMATCH;
347 934         1523 (my $check = $v) =~ tr/0-9//cd;
348 934 50       1492 return 0 if ! $check; # A string parsed as version (i.e.: AppleWebKit/en_SG)
349 934         1069 my $rv;
350             eval {
351 934         7422 $rv = version->new("$v")->numify;
352 934         3032 1;
353 934 50       1253 } or do {
354 0   0     0 my $error = $@ || '[unknown error while parsing version]';
355 0         0 if ( INSIDE_UNIT_TEST ) {
356 0         0 chomp $error;
357 0         0 if ( INSIDE_VERBOSE_TEST ) {
358             Test::More::diag( "[FATAL] _numify: version said: $error for '$v'" );
359             Test::More::diag(
360             sprintf '[FATAL] _numify: UA with bogus version (%s) is: %s',
361             $v, $self->[UA_STRING]
362             );
363             Test::More::diag( '[FATAL] _numify: ' . $self->dumper );
364             }
365 0         0 croak $error;
366             }
367             else {
368             croak $error;
369             }
370             };
371 934         4631 return $rv;
372             }
373              
374             sub _debug_pre_parse {
375 0     0     my($self, $moz, $thing, $extra, @others) = @_;
376              
377 0           my $raw = [
378             { qw/ name moz value / => $moz },
379             { qw/ name thing value / => $thing },
380             { qw/ name extra value / => $extra },
381             { qw/ name others value / => \@others },
382             ];
383 0           my $pok = print "-------------- PRE PARSE DUMP --------------\n"
384             . $self->dumper(args => $raw)
385             . "--------------------------------------------\n";
386 0           return;
387             }
388              
389             1;
390              
391             __END__