File Coverage

blib/lib/WWW/RobotRules/Parser/MultiValue.pm
Criterion Covered Total %
statement 106 106 100.0
branch 44 54 81.4
condition 7 13 53.8
subroutine 22 22 100.0
pod 5 5 100.0
total 184 200 92.0


line stmt bran cond sub pod time code
1             package WWW::RobotRules::Parser::MultiValue;
2 4     4   5278 use strict;
  4         4  
  4         129  
3 4     4   15 use warnings;
  4         4  
  4         98  
4 4     4   100 use 5.014;
  4         20  
  4         188  
5              
6             our $VERSION = '0.02';
7              
8             # core
9 4     4   18 use Scalar::Util qw(blessed);
  4         4  
  4         364  
10              
11             # cpan
12 4     4   9768 use URI;
  4         15203  
  4         132  
13 4     4   2217 use Text::Glob qw(match_glob);
  4         2705  
  4         230  
14 4     4   1763 use Hash::MultiValue;
  4         6802  
  4         134  
15             use Class::Accessor::Lite (
16 4         24 new => 1,
17             ro => [qw(agent ignore_default)],
18 4     4   2048 );
  4         3427  
19              
20             use constant {
21 4         4281 WILDCARD => 'wc',
22             ME => 'me',
23             TRANSLATOR => {
24             allow => '_translate_path_pattern',
25             disallow => '_translate_path_pattern',
26             'crawl-delay' => '_translate_delay',
27             'request-rate' => '_translate_rate',
28             },
29 4     4   344 };
  4         4  
30              
31             sub _uri {
32 115     115   112 my ($uri) = @_;
33 115 50 33     635 $uri = URI->new($uri.q()) unless blessed($uri) && $uri->isa('URI');
34 115 50 33     17024 return unless $uri->can('host') && $uri->can('port');
35 115         828 return $uri;
36             }
37              
38             sub _domain {
39 115     115   110 my ($uri) = @_;
40 115         204 return sprintf '%s:%d', $uri->host, $uri->port;
41             }
42              
43             sub _rules {
44 199     199   622 my ($self, $domain) = @_;
45 199   66     774 return $self->{rules}->{$domain} //= Hash::MultiValue->new;
46             }
47              
48             sub rules_for {
49 22     22 1 24 my ($self, $uri) = @_;
50 22 50       32 $uri = _uri($uri)
51             or return Hash::MultiValue->new;
52 22         143 my $path_query = $uri->path_query;
53 22         178 my $domain = _domain($uri);
54 22         715 return $self->_rules($domain);
55             }
56              
57             sub parse {
58 28     28 1 36006 my ($self, $robots_txt_uri, $txt) = @_;
59 28 50       54 $robots_txt_uri = _uri($robots_txt_uri)
60             or return;
61 28         172 my $domain = _domain($robots_txt_uri);
62              
63 28         981 my $ua = WILDCARD;
64 28         75 my $anon_rules = Hash::MultiValue->new;
65              
66 28   50     630 $txt = ($txt//'') =~ s|\r\n|\n|gr;
67 28         156 for my $line (split /[\r\n]/, $txt) {
68 202         2780 $line =~ s/(?:^\s*|\s*$|\s*[#].*$)//g;
69 202 100       454 next if $line =~ /^\s*$/; # skip empty line
70              
71 159 100       294 if ($line =~ /^User-Agent\s*:\s*(.*)$/i) {
72 41         68 $ua = $self->match_ua($1);
73             } else {
74 118 100       171 next unless $ua; # skip directives for other UA
75              
76 86 50       315 if ($line =~ /^([^:]+?)\s*:\s*(.*)$/) {
77 86         177 my ($rule, $value) = (lc $1, $2);
78 86 50       161 if (my $method = TRANSLATOR->{$rule}) {
79 86         212 ($rule, $value) = $self->$method(
80             $rule, $value, $robots_txt_uri,
81             );
82             }
83 86 100       159 next unless $rule;
84              
85 75 100       109 if ($ua eq ME) {
86 36         58 $self->_rules($domain)->add($rule => $value);
87             } else {
88 39         76 $anon_rules->add($rule => $value);
89             }
90             }
91             }
92             }
93              
94 28 100       281 unless ($self->ignore_default) {
95             # Add rules for default UA as a lower precedence
96             $self->_rules($domain)->add($_ => $anon_rules->get_all($_))
97 20         121 for $anon_rules->keys;
98             }
99              
100 28         489 return $self;
101             }
102              
103             sub match_ua {
104 53     53 1 3062 my ($self, $pattern) = @_;
105 53 100       118 return WILDCARD if $pattern eq '*';
106 37 100       59 return ME if index(lc $self->_short_agent, lc($pattern)) >= 0;
107 22         45 return undef;
108             }
109              
110             sub _match_path ($$) {
111 205     205   240 my ($str, $pattern) = @_;
112 205         235 local $Text::Glob::strict_leading_dot = 0;
113 205         146 local $Text::Glob::strict_wildcard_slash = 0;
114 205         419 return match_glob($pattern.'*', $str);
115             }
116              
117             sub allows {
118 65     65 1 3123 my ($self, $uri) = @_;
119 65 50       97 $uri = _uri($uri)
120             or return;
121 65         412 my $path_query = $uri->path_query;
122 65         510 my $domain = _domain($uri);
123 65         2067 for my $pattern ($self->_rules($domain)->get_all('allow')) {
124 139 100       18058 return 1 if _match_path $path_query, $pattern;
125             }
126 48         6610 for my $pattern ($self->_rules($domain)->get_all('disallow')) {
127 66 100       5907 return 0 if _match_path $path_query, $pattern;
128             }
129 25         1126 return 1;
130             }
131              
132             sub delay_for {
133 22     22 1 3941 my ($self, $uri, $base) = @_;
134 22         46 my ($delay) = $self->rules_for($uri)->get_all('crawl-delay');
135 22 100 100     345 $delay *= ( $base || 1 ) if defined $delay;
136 22         73 return $delay;
137             }
138              
139             sub _short_agent {
140 37     37   32 my ($self) = @_;
141 37         80 my $name = $self->agent;
142 37 50       300 $name = $1 if $name =~ m!^(\S+)!; # first word
143 37         271 $name =~ s!/.*$!!; # no version
144 37         159 return $name;
145             }
146              
147             sub _translate_path_pattern {
148 59     59   71 my ($self, $key, $value, $base_uri) = @_;
149              
150 59         41 my $ignore;
151 59         57 eval {
152 59         148 my $uri = URI->new_abs($value, $base_uri);
153 59 100       7295 $ignore++ unless $uri->scheme eq $base_uri->scheme;
154 59 50       899 $ignore++ unless lc($uri->host) eq lc($base_uri->host);
155 59 100       1608 $ignore++ unless $uri->port eq $base_uri->port;
156             };
157 59 50       1724 return () if $@;
158 59 100       93 return () if $ignore;
159              
160 52         115 return ($key, $value);
161             }
162              
163             sub _translate_delay { # into delay in milliseconds
164 11     11   24 my ($self, $key, $value) = @_;
165 11 100       69 return () unless $value =~ qr!\A[0-9.]+\z!;
166 10         26 return ('crawl-delay', $value);
167             }
168              
169             sub _translate_rate { # into delay in milliseconds
170 16     16   26 my ($self, $key, $value) = @_;
171 16 100       102 return () unless $value =~ qr!\A([0-9.]+)\s*/\s*([0-9.]+)\z!;
172 15 100       51 return () unless $1+0;
173 13         44 return ('crawl-delay', $2 / $1);
174             }
175              
176             1;
177             __END__