File Coverage

blib/lib/Net/PublicSuffixList.pm
Criterion Covered Total %
statement 157 187 83.9
branch 14 26 53.8
condition 9 15 60.0
subroutine 29 29 100.0
pod 17 17 100.0
total 226 274 82.4


line stmt bran cond sub pod time code
1             package Net::PublicSuffixList;
2 4     4   6693 use v5.26;
  4         18  
3 4     4   35 use strict;
  4         18  
  4         102  
4 4     4   24 use feature qw(signatures);
  4         9  
  4         453  
5 4     4   24 no warnings qw(experimental::signatures);
  4         10  
  4         165  
6              
7 4     4   22 use warnings;
  4         8  
  4         138  
8 4     4   23 no warnings;
  4         8  
  4         155  
9              
10 4     4   21 use Carp qw(carp);
  4         9  
  4         312  
11 4     4   29 use File::Basename qw(basename dirname);
  4         7  
  4         304  
12 4     4   45 use File::Path qw(make_path);
  4         9  
  4         315  
13 4     4   539 use File::Spec::Functions qw(catfile);
  4         791  
  4         8264  
14              
15             our $VERSION = '0.501';
16              
17             =encoding utf8
18              
19             =head1 NAME
20              
21             Net::PublicSuffixList - The Mozilla Public Suffix List
22              
23             =head1 SYNOPSIS
24              
25             use Net::PublicSuffixList;
26              
27             my $psl = Net::PublicSuffixList->new;
28              
29             my $host = 'amazon.co.uk';
30              
31             # get all the suffixes in host (like, uk and co.uk)
32             my $suffixes = $psl->suffixes_in( $host );
33              
34             # get the longest suffix
35             my $suffix = $psl->longest_suffix_in( $host );
36              
37             my $hash = $psl->split_host( $host );
38              
39             =head1 DESCRIPTION
40              
41             I mostly wrote this because I was working on L and needed a
42             way to figure out which part of a URL was the registered part and with
43             was the top-level domain.
44              
45             The Public Suffix List is essentially a self-reported collection of the
46             top-level, generic, country code, or whatever domains.
47              
48             There are other modules that try to do this, but they come with packaged
49             (old) versions of the Public Suffix List or have limited functionality.
50              
51             This module can fetch the most current one for you, use one that you
52             provide locally, or even let you completely make it up. You can add
53             entries you want but don't show up in the list, and remove ones you don't
54             think should be there.
55              
56             =over 4
57              
58             =item new
59              
60             Create the new object and specify how you'd like to get the data. The
61             network file is about 220Kb, so you might want to fetch it once, store
62             it, and then use C to use it.
63              
64             The constructor first tries to use a local file. If you've disabled
65             that with C or the file doesn't exist, it moves on to trying
66             the network. If you've disabled the network with C, then it
67             complains but still returns the object. You can still construct your
68             own list with C.
69              
70             Possible keys:
71              
72             list_url # the URL for the suffix list
73             local_path # the path to a local file that has the suffix list
74             no_net # do not use the network
75             no_local # do not use a local file
76             cache_dir # location to save the fetched file
77              
78             =cut
79              
80 11     11 1 27531 sub new ( $class, %args ) {
  11         22  
  11         36  
  11         18  
81 11         22 my $self = bless {}, $class;
82 11         77 $self->_init( \%args );
83             }
84              
85 11     11   21 sub _init ( $self, $args ) {
  11         20  
  11         16  
  11         16  
86 11         22 my %args = ( $self->defaults->%*, $args->%* );
87              
88 11         64 while( my($k, $v) = each %args ) {
89 55         121 $self->{$k} = $v;
90 55 100       161 if( $k eq 'local_path' ) {
91 11         446 $self->{local_file} = basename( $v );
92             }
93             }
94              
95 11         20 my $method = do {
96 11 100 66     47 if( ! $self->{no_local} and -e $self->local_path ) {
    50          
97 2         9 'fetch_list_from_local'
98             }
99             elsif( ! $self->{no_net} ) {
100 0         0 'fetch_list_from_net'
101             }
102             else {
103 9         1975 carp "No way to fetch list! Check your settings for no_local or no_net";
104 9         87 return $self;
105             }
106             };
107              
108 2         11 my $ref = $self->$method();
109              
110 2         9 $self->parse_list( $ref );
111              
112 2         10 $self;
113             }
114              
115             =item defaults
116              
117             A hash of the default values for everything.
118              
119             =cut
120              
121 11     11 1 22 sub defaults ( $self ) {
  11         16  
  11         20  
122             state $hash = {
123             list_url => $self->default_url,
124             local_path => $self->default_local_path,
125             no_net => 0,
126             no_local => 0,
127 11         32 cache_dir => catfile( $ENV{HOME}, '.publicsuffixlist' ),
128             };
129 11         78 $hash;
130             }
131              
132             =item parse_list( STRING_REF )
133              
134             Take a scalar reference to the contents of the public suffix list,
135             find all the suffices and add them to the object.
136              
137             =cut
138              
139 7     7 1 8665 sub parse_list ( $self, $list ) {
  6         11  
  6         9  
  6         10  
140 6 100       22 unless( ref $list eq 'SCALAR' ) {
141 2         233 carp "Argument is not a scalar reference";
142 2         111 return;
143             }
144              
145 4         24 my( $line_ending ) = $$list =~ m/(\R)/;
146 4     1   86 open my $string_fh, '<:utf8', $list;
  1         12  
  1         3  
  1         9  
147 4         1006 $string_fh->input_record_separator( $line_ending );
148              
149 4         807 while( <$string_fh> ) {
150 28         48 chomp;
151 28 100 100     178 next if( /\A\s*\z/ || m|\A\s*//| );
152 20         45 s/\A\Q*.//;
153 20         59 $self->add_suffix( $_ );
154             }
155 4         22 $self;
156             }
157              
158             =item add_suffix( STRING )
159              
160             Add STRING to the known public suffices. This returns the object itself.
161              
162             Before this adds the suffix, it strips off leading C<*> and C<.*>
163             characters. Some sources specify C<*.foo.bar>, but this adds C.
164              
165             =cut
166              
167 26     26 1 1447 sub add_suffix ( $self, $suffix ) {
  26         44  
  26         37  
  26         32  
168 26         70 $suffix =~ s/\A[*.]+//;
169 26         78 $self->{suffix}{$suffix}++;
170 26         67 $self
171             }
172              
173             =item remove_suffix( STRING )
174              
175             Remove the STRING as a known public suffices. This returns the object
176             itself.
177              
178             =cut
179              
180 1     1 1 4 sub remove_suffix ( $self, $suffix ) { delete $self->{suffix}{$suffix}; $self }
  1         2  
  1         2  
  1         3  
  1         2  
  1         3  
181              
182             =item suffix_exists( STRING )
183              
184             Return the invocant if the suffix exists, and the empty list otherwise.
185              
186             =cut
187              
188 50 100   50 1 14015 sub suffix_exists ( $self, $suffix ) { exists $self->{suffix}{$suffix} ? $self : () }
  50         84  
  50         76  
  50         72  
  50         315  
189              
190             =item suffixes_in( HOST )
191              
192             Return an array reference of the publix suffixes in HOST, sorted from
193             shortest to longest.
194              
195             =cut
196              
197 3     3 1 2035 sub suffixes_in ( $self, $host ) {
  3         5  
  3         6  
  3         4  
198 3         13 my @parts = reverse split /\./, $host;
199             my @suffixes =
200 6         14 map { $_->[0] }
201 9         19 grep { $_->[1] }
202 9         17 map { [ $_, $self->suffix_exists( $_ ) ] }
203 3         9 map { join '.', reverse @parts[0..$_] }
  9         32  
204             0 .. $#parts;
205              
206 3         14 \@suffixes;
207             }
208              
209             =item longest_suffix_in( HOST )
210              
211             Return the longest public suffix in HOST.
212              
213             =cut
214              
215 2     2 1 4111 sub longest_suffix_in ( $self, $host ) {
  2         4  
  2         4  
  2         3  
216 2         10 $self->suffixes_in( $host )->@[-1];
217             }
218              
219             =item split_host( HOST )
220              
221             Returns a hash reference with these keys:
222              
223             host the input value
224             suffix the longest public suffix
225             short the input value with the public suffix
226             (and leading dot) removed
227              
228             =cut
229              
230 1     1 1 3163 sub split_host ( $self, $host ) {
  1         3  
  1         2  
  1         2  
231 1         3 my $suffix = $self->longest_suffix_in( $host );
232 1         31 my $short = $host =~ s/\Q.$suffix\E\z//r;
233              
234             return {
235 1         21 host => $host,
236             suffix => $suffix,
237             short => $short
238             }
239             }
240              
241             =item fetch_list_from_local
242              
243             Fetch the public suffix list plaintext file from the path returned
244             by C. Returns a scalar reference to the text of the raw
245             UTF-8 octets.
246              
247             =cut
248              
249 3     3 1 1163 sub fetch_list_from_local ( $self ) {
  3         6  
  3         5  
250 3 100       11 return if $self->{no_local};
251 2         14 open my $fh, '<:raw', $self->local_path;
252 2         9 my $data = do { local $/; <$fh> };
  2         10  
  2         112  
253 2         10 $self->{source} = 'local_file';
254 2         30 \$data;
255             }
256              
257             =item fetch_list_from_net
258              
259             Fetch the public suffix list plaintext file from the URL returned
260             by C. Returns a scalar reference to the text of the raw
261             UTF-8 octets.
262              
263             If you've set C in the object, this method attempts to
264             cache the response in that directory using C as
265             the filename. This cache is different than C although you
266             can use it as C.
267              
268             =cut
269              
270 1     1 1 12 sub fetch_list_from_net ( $self ) {
  1         4  
  1         2  
271 1 50       9 return if $self->{no_net};
272 0         0 state $rc = require Mojo::UserAgent;
273 0         0 state $ua = Mojo::UserAgent->new;
274              
275 0         0 my $path = catfile( $self->{cache_dir}, $self->default_local_file );
276 0         0 my $local_last_modified = (stat $path)[9];
277 0         0 my $headers = {};
278              
279 0 0       0 if( $self->{cache_dir} ) {
280 0         0 make_path $self->{cache_dir};
281 0 0       0 if( $local_last_modified ) {
282 0         0 $headers->{'If-Modified-Since'} = Mojo::Date->new($local_last_modified);
283             }
284             }
285              
286 0         0 my $tx = $ua->get( $self->url() => $headers );
287              
288 0         0 my $body;
289 0 0       0 if( $tx->result->code eq '304' ) {
    0          
290 0         0 open my $fh, '<:raw', $path;
291 0         0 $body = do { local $/; <$fh> };
  0         0  
  0         0  
292 0         0 close $fh;
293 0         0 $self->{source} = 'net_cached';
294             }
295             elsif( $tx->result->code eq '200' ) {
296 0         0 $body = eval { $tx->result->body };
  0         0  
297              
298 0         0 my $date = Mojo::Date->new(
299             $tx->result->headers->last_modified,
300             $tx->result->headers->date,
301             0
302             );
303              
304 0 0       0 if( $self->{cache_dir} ) {
305 0         0 open my $fh, '>:raw', $path;
306 0         0 print { $fh } $body;
  0         0  
307 0         0 close $fh;
308 0         0 utime $date->epoch, $date->epoch, $path;
309             }
310              
311 0         0 $self->{source} = 'net';
312             }
313              
314 0         0 return \$body;
315             }
316              
317             =item url
318              
319             Return the configured URL for the public suffix list.
320              
321             =cut
322              
323 2     2 1 1249 sub url ( $self ) {
  2         4  
  2         4  
324 2   66     11 $self->{list_url} // $self->default_url
325             }
326              
327             =item default_url
328              
329             Return the default URL for the public suffix list.
330              
331             =cut
332              
333 5     5 1 9 sub default_url ( $self ) {
  5         8  
  5         8  
334 5         22 'https://publicsuffix.org/list/public_suffix_list.dat'
335             }
336              
337             =item local_path
338              
339             Return the configured local path for the public suffix list.
340              
341             =cut
342              
343 5     5 1 16 sub local_path ( $self ) {
  5         9  
  5         6  
344 5   33     140 $self->{local_path} // $self->default_local_path
345             }
346              
347             =item default_local_path
348              
349             Return the default local path for the public suffix list.
350              
351             =cut
352              
353 3     3 1 5 sub default_local_path ( $self ) {
  3         6  
  3         5  
354 3         6 my $this_file = __FILE__;
355 3         196 my $this_dir = dirname( $this_file );
356 3         13 my $file = catfile( $this_dir, $self->default_local_file );
357             }
358              
359             =item local_file
360              
361             Return the configured filename for the public suffix list.
362              
363             =cut
364              
365 1     1 1 810 sub local_file ( $self ) {
  1         3  
  1         2  
366 1   33     7 $self->{local_file} // $self->default_local_file
367             }
368              
369             =item default_local_file
370              
371             Return the default filename for the public suffix list.
372              
373             =cut
374              
375 3     3 1 5 sub default_local_file ( $self ) {
  3         6  
  3         5  
376 3         38 'public_suffix_list.dat'
377             }
378              
379             =back
380              
381             =head1 TO DO
382              
383              
384             =head1 SEE ALSO
385              
386             L, L, L
387              
388             =head1 SOURCE AVAILABILITY
389              
390             This source is in Github:
391              
392             http://github.com/briandfoy/net-publicsuffixlist
393              
394             =head1 AUTHOR
395              
396             brian d foy, C<< >>
397              
398             =head1 COPYRIGHT AND LICENSE
399              
400             Copyright © 2020-2021, brian d foy, All Rights Reserved.
401              
402             You may redistribute this under the terms of the Artistic License 2.0.
403              
404             =cut
405              
406             1;