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.40_02'; # TRIAL
3              
4 2     2   78935 $Parse::HTTP::UserAgent::VERSION = '0.4002';use strict;
  2         4  
  2         53  
5 2     2   8 use warnings;
  2         4  
  2         65  
6              
7 2         619 use base qw(
8             Parse::HTTP::UserAgent::Base::IS
9             Parse::HTTP::UserAgent::Base::Parsers
10             Parse::HTTP::UserAgent::Base::Dumper
11             Parse::HTTP::UserAgent::Base::Accessors
12 2     2   9 );
  2         3  
13              
14 2         7 use overload '""', => 'name',
15             '0+', => 'version',
16             fallback => 1,
17 2     2   10 ;
  2         3  
18              
19 2     2   647 use version;
  2         2735  
  2         8  
20 2     2   135 use Carp qw( croak );
  2         4  
  2         90  
21 2     2   11 use Parse::HTTP::UserAgent::Constants qw(:all);
  2         4  
  2         554  
22              
23             BEGIN {
24 2 50   2   4063 constant->import( DEBUG => 0 ) if not defined &DEBUG;
25             }
26              
27             my %OSFIX = (
28             'WinNT4.0' => 'Windows NT 4.0',
29             'WinNT' => 'Windows NT',
30             'Windows 4.0' => 'Windows 95',
31             'Win95' => 'Windows 95',
32             'Win98' => 'Windows 98',
33             'Windows 4.10' => 'Windows 98',
34             'Win 9x 4.90' => 'Windows Me',
35             'Windows NT 5.0' => 'Windows 2000',
36             'Windows NT 5.1' => 'Windows XP',
37             'Windows XP 5.1' => 'Windows XP', # huh?
38             'Windows NT 5.2' => 'Windows Server 2003',
39             'Windows NT 6.0' => 'Windows Vista / Server 2008',
40             'Windows NT 6.1' => 'Windows 7',
41             'Windows NT 6.2' => 'Windows 8',
42             'Windows NT 6.3' => 'Windows 8.1',
43             );
44              
45             sub new {
46 554     554 1 540659 my $class = shift;
47 554   33     1491 my $ua = shift || croak 'No user agent string specified';
48 554   50     2151 my $opt = shift || {};
49 554 50       1673 croak 'Options must be a hash reference' if ref $opt ne 'HASH';
50 554         1327 my $self = [ map { undef } 0..MAXID ];
  14958         23916  
51 554         1359 bless $self, $class;
52 554         1164 @{ $self }[ UA_STRING, UA_STRING_ORIGINAL ] = ($ua) x 2;
  554         1665  
53 554 50       1517 $self->[IS_EXTENDED] = exists $opt->{extended} ? $opt->{extended} : 1;
54 554 50       1266 $self->_normalize( $opt->{normalize} ) if $opt->{normalize};
55 554         1521 $self->_parse;
56 554         1453 return $self;
57             }
58              
59             sub as_hash {
60 277     277 1 933 my $self = shift;
61 277         385 my %struct;
62 277         480 foreach my $id ( $self->_object_ids ) {
63 6371         15709 (my $name = $id) =~ s{ \A UA_ }{}xms;
64 6371         17294 $struct{ lc $name } = $self->[ $self->$id() ];
65             }
66 277         3820 return %struct;
67             }
68              
69             sub trim {
70 1352     1352 1 1809 my $self = shift;
71 1352         1730 my $s = shift;
72 1352 50       2079 return $s if ! $s;
73 1352         2485 $s =~ s{ \A \s+ }{}xms;
74 1352         2328 $s =~ s{ \s+ \z }{}xms;
75 1352         2677 return $s;
76             }
77              
78             sub _normalize {
79 0     0   0 my $self = shift;
80 0         0 my $nopt = shift;
81 0         0 my $type = ref $nopt;
82              
83             my @o = ! $type ? ':all'
84 0 0       0 : $type eq 'ARRAY' ? @{ $nopt }
  0 0       0  
85             : croak "Normalization option $nopt is invalid";
86              
87 0         0 my %mode = map { $_ => 1 } @o;
  0         0  
88 0         0 my @all = qw( plus_to_space trim_spaces );
89 0 0       0 @mode{ @all } = (1) x @all if delete $mode{':all'};
90              
91 0         0 my $s = \$self->[UA_STRING];
92 0 0       0 ${$s} =~ s{[+]}{ }xmsg if $mode{plus_to_space};
  0         0  
93 0 0       0 ${$s} =~ s<\s+>< >xmsg if $mode{trim_spaces};
  0         0  
94 0         0 return;
95             }
96              
97             sub _parse {
98 554     554   849 my $self = shift;
99 554 50       1165 return $self if $self->[IS_PARSED];
100 554         1180 $self->_do_parse( $self->_pre_parse );
101 554         1184 $self->[IS_PARSED] = 1;
102 554 50       1785 $self->_post_parse if ! $self->[UA_UNKNOWN];
103 554         716 return;
104             }
105              
106             sub _pre_parse {
107 554     554   799 my $self = shift;
108 554         826 my $ua = $self->[UA_STRING];
109 554         1214 my $uc_ua = uc $ua;
110              
111 554         1436 $self->[IS_MAXTHON] = index($uc_ua, 'MAXTHON') != NO_IMATCH;
112 554         1034 $self->[IS_TRIDENT] = index($uc_ua, 'TRIDENT/') != NO_IMATCH;
113              
114 554         755 my @parts;
115 554         681 my $i = 0;
116 554         770 my $depth = 0;
117              
118 554         4544 foreach my $token ( split RE_SPLIT_PARSE, $ua ) {
119 3000 100       5210 if ( $token eq '(' ) {
120 680 100       1226 $i++ if ++$depth == 1;
121 680         1101 next;
122             }
123 2320 100       3514 if ( $token eq ')' ) {
124 678 100       1478 $i++ if --$depth == 0;
125 678         960 next;
126             }
127 1642   100     1894 push @{ $parts[$i] ||= [] }, $token;
  1642         5565  
128             }
129              
130             # Hopefully the above code was successful and now we can set the actual
131             # tokens to use inside parsers.
132 554 50       1046 my($moz) = join ' ', @{ shift(@parts) || [] };
  554         1773  
133 554 100       1009 my($thing) = join ' ', @{ shift(@parts) || [] };
  554         1412  
134 554 100       952 my($extra) = join ' ', @{ shift(@parts) || [] };
  554         1500  
135 554         1034 my(@others) = map { @{ $_ } } @parts;
  250         315  
  250         527  
136              
137 554 100       3143 $thing = $thing ? [ split RE_SC_WS, $thing ] : [];
138 554 100       1772 $extra = [ split RE_WHITESPACE, $extra ] if $extra;
139              
140 554         745 $self->_debug_pre_parse( $moz, $thing, $extra, @others ) if DEBUG;
141 554         2129 return $moz, $thing, $extra, @others;
142             }
143              
144             sub _do_parse {
145 554     554   1169 my($self, $m, $t, $e, @o) = @_;
146              
147 554   100     1881 my $c = $t->[0] && $t->[0] eq 'compatible';
148              
149 554 100 66     1187 if ( $c
      100        
      100        
      100        
150 212         1208 && shift @{$t} # just inline removal of "compatible"
151             && ( ! $e || $self->[IS_TRIDENT] ) # older versions don't have junk outside, while newer might have
152             && ! $self->[IS_MAXTHON] # be sure that this is not the faker
153             ) {
154 150         544 my($n, $v) = split RE_WHITESPACE, $t->[0];
155 150 100 100     575 if ( $n eq 'MSIE' && index($m, q{ }) == NO_IMATCH ) {
156 120         404 return $self->_parse_msie($m, $t, $e, $n, $v);
157             }
158             }
159              
160 434 100       869 if ( $self->[IS_TRIDENT] ) {
161             # http://blogs.msdn.com/b/ieinternals/archive/2013/09/21/internet-explorer-11-user-agent-string-ua-string-sniffing-compatibility-with-gecko-webkit.aspx
162             my %msie11 = map {
163 164 100       448 index( $_, 'Windows') != NO_IMATCH ? ( windows => 1 )
    100          
    100          
164             : index( $_, 'Trident/') != NO_IMATCH ? ( trident => 1 )
165             : index( $_, 'rv:') != NO_IMATCH ? ( version => 1 )
166             : ()
167 26         41 } @{ $t };
  26         50  
168 26         60 my $msie_matched = keys %msie11;
169              
170 26 100 66     131 if ( $msie_matched == 3 ){
    100 66        
171 12         56 return $self->_parse_msie_11($m, $t, $e);
172             }
173             elsif ( ! $self->[IS_MAXTHON] && $msie_matched == 2 && ! $msie11{version} ) {
174             # another weird case. robot?
175 2         5 my(@buf, $vstr);
176 2         5 for my $junk ( @{ $t } ) {
  2         5  
177 8 100       17 if ( index( $junk, 'MSIE') != NO_IMATCH ) {
178 2         6 $vstr = $junk;
179 2         4 next;
180             }
181 6         12 push @buf, $junk;
182             }
183              
184 2         12 my $rv = $self->_parse_msie($m, \@buf, $e, split( RE_WHITESPACE, $vstr ) );
185 2         7 return $rv;
186             }
187             # fall back to the dispatch table below
188             }
189              
190 420 100       1525 my $rv = $self->[IS_MAXTHON] ? [ maxthon => $m, $t, $e, @o ]
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
191             : $self->_is_opera_pre($m) ? [ opera_pre => $m, $t, $e ]
192             : $self->_is_opera_post($e) ? [ opera_post => $m, $t, $e, $c ]
193             : $self->_is_opera_ff($e) ? [ opera_pre => "$e->[2]/$e->[3]", $t ]
194             : $self->_is_ff($e) ? [ firefox => $m, $t, $e, @o ]
195             : $self->_is_safari($e, \@o) ? [ safari => $m, $t, $e, @o ]
196             : $self->_is_chrome($e, \@o) ? [ chrome => $m, $t, $e, @o ]
197             : $self->_is_android($t,\@o) ? [ android => $m, $t, $e, @o ]
198             : $self->_is_suspicious_ff($e) ? [ ff_suspect => $m, $t, $e, @o ]
199             : undef;
200              
201 420 100       1017 if ( $rv ) {
202 234         301 my $pname = shift @{ $rv };
  234         405  
203 234         468 my $method = '_parse_' . $pname;
204 234         346 my $rvx = $self->$method( @{ $rv } );
  234         967  
205 234 100       484 if ( $rvx ) {
206 232   66     763 $self->[UA_PARSER] ||= $pname;
207 232         589 return $rvx;
208             }
209             }
210              
211 188 50       594 return $self->_extended_probe($m, $t, $e, $c, @o) if $self->[IS_EXTENDED];
212              
213 0         0 $self->[UA_UNKNOWN] = 1; # give up
214 0         0 return;
215             }
216              
217             sub _post_parse {
218 554     554   775 my $self = shift;
219 554 100       1356 $self->[UA_VERSION] = $self->_numify( $self->[UA_VERSION_RAW] )
220             if $self->[UA_VERSION_RAW];
221              
222 554         934 my @buf;
223 554         713 foreach my $e ( @{ $self->[UA_EXTRAS] } ) {
  554         1252  
224 860 100       1851 if ( $self->_is_strength( $e ) ) {
225 76         137 $self->[UA_STRENGTH] = $e ;
226 76         156 next;
227             }
228 784         1514 push @buf, $e;
229             }
230              
231 554 100       1537 $self->[UA_EXTRAS] = @buf ? [ @buf ] : undef;
232              
233 554 100       1134 if ( $self->[UA_TOOLKIT] ) {
234 258         459 my $v = $self->[UA_TOOLKIT][TK_ORIGINAL_VERSION];
235 258 100       320 push @{ $self->[UA_TOOLKIT] }, defined $v ? $self->_numify( $v ) : 0;
  258         689  
236             }
237              
238 554 100       1101 if( $self->[UA_MOZILLA] ) {
239 88         156 $self->[UA_MOZILLA] =~ tr/a-z://d;
240 88         177 $self->[UA_MOZILLA] = [ $self->[UA_MOZILLA],
241             $self->_numify( $self->[UA_MOZILLA] ) ];
242             }
243              
244 554 100       992 if ( $self->[UA_OS] ) {
245 490   66     1507 $self->[UA_OS] = $OSFIX{ $self->[UA_OS] } || $self->[UA_OS];
246             }
247              
248 554         1169 foreach my $robo ( LIST_ROBOTS ) { # regex???
249 4908 100       10140 next if lc $robo ne lc $self->[UA_NAME];
250 18         29 $self->[UA_ROBOT] = 1;
251 18         29 last;
252             }
253 554         928 return;
254             }
255              
256             sub _extended_probe {
257 188     188   486 my($self, @args) = @_;
258              
259 188 100 66     413 return if $self->_is_gecko && $self->_parse_gecko( @args );
260 138 100 66     369 return if $self->_is_netscape( @args ) && $self->_parse_netscape( @args );
261 112 100 66     301 return if $self->_is_docomo( @args ) && $self->_parse_docomo( @args );
262 110 100       290 return if $self->_is_generic( @args );
263 26 100 66     73 return if $self->_is_emacs( @args ) && $self->_parse_emacs( @args );
264 22 100 66     66 return if $self->_is_moz_only( @args ) && $self->_parse_moz_only( @args );
265 2 50 33     23 return if $self->_is_hotjava( @args ) && $self->_parse_hotjava( @args );
266              
267 0         0 $self->[UA_UNKNOWN] = 1;
268 0         0 return;
269             }
270              
271             sub _object_ids {
272 554     554   10619 return grep { $_ =~ RE_OBJECT_ID } keys %Parse::HTTP::UserAgent::;
  74954         122381  
273             }
274              
275             sub _numify {
276 916     916   1252 my $self = shift;
277 916   50     1645 my $v = shift || return 0;
278 916         1219 my @removed;
279              
280 916 100       4014 if (
281             $v =~ s{(
282             pre |
283             rel |
284             alpha |
285             beta |
286             \-stable |
287             gold |
288             [ab]\d+ |
289             a\-XXXX |
290             dev |
291             [+]
292             )}{}xmsig
293             ){
294 46         80 push @removed, $1 if INSIDE_VERBOSE_TEST;
295             }
296              
297 916 100       3174 if (
298             $v =~ s{(
299             (?:[^0-9]+)? # usually dash
300             rc # nonsense
301             [\-_.]? # usually dash
302             ([0-9]) # teh candidate revision
303             )}{.0.$2}xmsi # yeah, hacky
304             ) {
305 2         5 push @removed, $1 if INSIDE_VERBOSE_TEST;
306             }
307              
308             # workaround another stupidity (1.2.3-4)
309 916 100       2009 if ( my $rc = $v =~ tr/-/./ ) {
310 2         4 push @removed, '-' x $rc if INSIDE_VERBOSE_TEST;
311             }
312              
313             # convert _ to .
314             # version.pm has changed its interpretation of versions with underlines
315             # cf. https://bugs.debian.org/825611
316 916 100       1705 if ( my $rc = $v =~ tr/_/./ ) {
317 2         4 push @removed, '-' x $rc if INSIDE_VERBOSE_TEST;
318             }
319              
320             # Finally, be aggressive to prevent dying on bogus stuff.
321             # It's interesting how people provide highly stupid version "numbers".
322             # Version parameters are probably more stupid than the UA string itself.
323 916 100       2006 if ( $v =~ s<([^0-9._v])><.>xmsg ) {
324 2         6 push @removed, $1 if INSIDE_VERBOSE_TEST;
325             }
326              
327 916 50       1838 if ( $v =~ s<([.]{2,})><.>xmsg ) {
328 0         0 push @removed, $1 if INSIDE_VERBOSE_TEST;
329             }
330              
331 916         1073 if ( INSIDE_VERBOSE_TEST ) {
332             if ( @removed ) {
333             my $r = join q{','}, @removed;
334             require Test::More;
335             Test::More::diag("[DEBUG] _numify: removed '$r' from version string");
336             }
337             }
338              
339             # Gecko revisions like: "20080915000512" will cause an
340             # integer overflow warning. use bigint?
341             local $SIG{__WARN__} = sub {
342 2     2   5 my $msg = shift;
343 2 50 33     22 warn "$msg\n" if $msg !~ RE_WARN_OVERFLOW && $msg !~ RE_WARN_INVALID;
344 916         5229 };
345             # if version::vpp is used it'll identify 420 as a v-string
346             # add a floating point to fool it
347 916 100       2350 $v .= q{.0} if index($v, q{.}) == NO_IMATCH;
348 916         1564 (my $check = $v) =~ tr/0-9//cd;
349 916 50       1624 return 0 if ! $check; # A string parsed as version (i.e.: AppleWebKit/en_SG)
350 916         1096 my $rv;
351             eval {
352 916         7985 $rv = version->new("$v")->numify;
353 916         3046 1;
354 916 50       1220 } or do {
355 0   0     0 my $error = $@ || '[unknown error while parsing version]';
356 0         0 if ( INSIDE_UNIT_TEST ) {
357 0         0 chomp $error;
358 0         0 if ( INSIDE_VERBOSE_TEST ) {
359             Test::More::diag( "[FATAL] _numify: version said: $error for '$v'" );
360             Test::More::diag(
361             sprintf '[FATAL] _numify: UA with bogus version (%s) is: %s',
362             $v, $self->[UA_STRING]
363             );
364             Test::More::diag( '[FATAL] _numify: ' . $self->dumper );
365             }
366 0         0 croak $error;
367             }
368             else {
369             croak $error;
370             }
371             };
372 916         4795 return $rv;
373             }
374              
375             sub _debug_pre_parse {
376 0     0     my($self, $moz, $thing, $extra, @others) = @_;
377              
378 0           my $raw = [
379             { qw/ name moz value / => $moz },
380             { qw/ name thing value / => $thing },
381             { qw/ name extra value / => $extra },
382             { qw/ name others value / => \@others },
383             ];
384 0           my $pok = print "-------------- PRE PARSE DUMP --------------\n"
385             . $self->dumper(args => $raw)
386             . "--------------------------------------------\n";
387 0           return;
388             }
389              
390             1;
391              
392             __END__