File Coverage

blib/lib/WWW/RobotRules/Parser/MultiValue.pm
Criterion Covered Total %
statement 105 105 100.0
branch 42 52 80.7
condition 7 13 53.8
subroutine 22 22 100.0
pod 5 5 100.0
total 181 197 91.8


line stmt bran cond sub pod time code
1             package WWW::RobotRules::Parser::MultiValue;
2 3     3   3328 use strict;
  3         5  
  3         98  
3 3     3   12 use warnings;
  3         3  
  3         79  
4 3     3   76 use 5.014;
  3         14  
  3         123  
5              
6             our $VERSION = '0.01';
7              
8             # core
9 3     3   12 use Scalar::Util qw(blessed);
  3         4  
  3         278  
10              
11             # cpan
12 3     3   1527 use URI;
  3         10291  
  3         118  
13 3     3   1329 use Text::Glob qw(match_glob);
  3         2040  
  3         148  
14 3     3   1329 use Hash::MultiValue;
  3         4843  
  3         95  
15             use Class::Accessor::Lite (
16 3         17 new => 1,
17             ro => [qw(agent)],
18 3     3   1282 );
  3         2530  
19              
20             use constant {
21 3         3124 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 3     3   229 };
  3         6  
30              
31             sub _uri {
32 92     92   88 my ($uri) = @_;
33 92 50 33     549 $uri = URI->new($uri.q()) unless blessed($uri) && $uri->isa('URI');
34 92 50 33     10816 return unless $uri->can('host') && $uri->can('port');
35 92         577 return $uri;
36             }
37              
38             sub _domain {
39 92     92   102 my ($uri) = @_;
40 92         201 return sprintf '%s:%d', $uri->host, $uri->port;
41             }
42              
43             sub _rules {
44 172     172   729 my ($self, $domain) = @_;
45 172   66     752 return $self->{rules}->{$domain} //= Hash::MultiValue->new;
46             }
47              
48             sub rules_for {
49 16     16 1 16 my ($self, $uri) = @_;
50 16 50       23 $uri = _uri($uri)
51             or return Hash::MultiValue->new;
52 16         117 my $path_query = $uri->path_query;
53 16         142 my $domain = _domain($uri);
54 16         543 return $self->_rules($domain);
55             }
56              
57             sub parse {
58 20     20 1 39258 my ($self, $robots_txt_uri, $txt) = @_;
59 20 50       44 $robots_txt_uri = _uri($robots_txt_uri)
60             or return;
61 20         142 my $domain = _domain($robots_txt_uri);
62              
63 20         823 my $ua = WILDCARD;
64 20         68 my $anon_rules = Hash::MultiValue->new;
65              
66 20   50     548 $txt = ($txt//'') =~ s|\r\n|\n|gr;
67 20         137 for my $line (split /[\r\n]/, $txt) {
68 135         2063 $line =~ s/(?:^\s*|\s*$|\s*[#].*$)//g;
69 135 100       319 next if $line =~ /^\s*$/; # skip empty line
70              
71 108 100       216 if ($line =~ /^User-Agent\s*:\s*(.*)$/i) {
72 17         33 $ua = $self->match_ua($1);
73             } else {
74 91 100       154 next unless $ua; # skip directives for other UA
75              
76 69 50       285 if ($line =~ /^([^:]+?)\s*:\s*(.*)$/) {
77 69         213 my ($rule, $value) = (lc $1, $2);
78 69 50       148 if (my $method = TRANSLATOR->{$rule}) {
79 69         187 ($rule, $value) = $self->$method(
80             $rule, $value, $robots_txt_uri,
81             );
82             }
83 69 100       144 next unless $rule;
84              
85 58 100       102 if ($ua eq ME) {
86 30         54 $self->_rules($domain)->add($rule => $value);
87             } else {
88 28         72 $anon_rules->add($rule => $value);
89             }
90             }
91             }
92             }
93              
94             # Add rules for default UA as a lower precedence
95             $self->_rules($domain)->add($_ => $anon_rules->get_all($_))
96 20         309 for $anon_rules->keys;
97              
98 20         541 return $self;
99             }
100              
101             sub match_ua {
102 29     29 1 3958 my ($self, $pattern) = @_;
103 29 100       97 return WILDCARD if $pattern eq '*';
104 21 100       35 return ME if index(lc $self->_short_agent, lc($pattern)) >= 0;
105 12         32 return undef;
106             }
107              
108             sub _match_path ($$) {
109 198     198   263 my ($str, $pattern) = @_;
110 198         198 local $Text::Glob::strict_leading_dot = 0;
111 198         154 local $Text::Glob::strict_wildcard_slash = 0;
112 198         457 return match_glob($pattern.'*', $str);
113             }
114              
115             sub allows {
116 56     56 1 3155 my ($self, $uri) = @_;
117 56 50       89 $uri = _uri($uri)
118             or return;
119 56         405 my $path_query = $uri->path_query;
120 56         518 my $domain = _domain($uri);
121 56         2067 for my $pattern ($self->_rules($domain)->get_all('allow')) {
122 136 100       20671 return 1 if _match_path $path_query, $pattern;
123             }
124 42         7562 for my $pattern ($self->_rules($domain)->get_all('disallow')) {
125 62 100       6422 return 0 if _match_path $path_query, $pattern;
126             }
127 22         1046 return 1;
128             }
129              
130             sub delay_for {
131 16     16 1 4722 my ($self, $uri, $base) = @_;
132 16         34 my ($delay) = $self->rules_for($uri)->get_all('crawl-delay');
133 16 100 100     285 $delay *= ( $base || 1 ) if defined $delay;
134 16         56 return $delay;
135             }
136              
137             sub _short_agent {
138 21     21   21 my ($self) = @_;
139 21         51 my $name = $self->agent;
140 21 50       198 $name = $1 if $name =~ m!^(\S+)!; # first word
141 21         46 $name =~ s!/.*$!!; # no version
142 21         101 return $name;
143             }
144              
145             sub _translate_path_pattern {
146 50     50   66 my ($self, $key, $value, $base_uri) = @_;
147              
148 50         36 my $ignore;
149 50         49 eval {
150 50         122 my $uri = URI->new_abs($value, $base_uri);
151 50 100       6935 $ignore++ unless $uri->scheme eq $base_uri->scheme;
152 50 50       834 $ignore++ unless lc($uri->host) eq lc($base_uri->host);
153 50 100       1554 $ignore++ unless $uri->port eq $base_uri->port;
154             };
155 50 50       1519 return () if $@;
156 50 100       95 return () if $ignore;
157              
158 43         104 return ($key, $value);
159             }
160              
161             sub _translate_delay { # into delay in milliseconds
162 7     7   12 my ($self, $key, $value) = @_;
163 7 100       54 return () unless $value =~ qr!\A[0-9.]+\z!;
164 6         22 return ('crawl-delay', $value);
165             }
166              
167             sub _translate_rate { # into delay in milliseconds
168 12     12   25 my ($self, $key, $value) = @_;
169 12 100       108 return () unless $value =~ qr!\A([0-9.]+)\s*/\s*([0-9.]+)\z!;
170 11 100       50 return () unless $1+0;
171 9         35 return ('crawl-delay', $2 / $1);
172             }
173              
174             1;
175             __END__