File Coverage

blib/lib/Net/PublicSuffixList.pm
Criterion Covered Total %
statement 154 184 83.7
branch 14 26 53.8
condition 9 15 60.0
subroutine 28 28 100.0
pod 17 17 100.0
total 222 270 82.2


line stmt bran cond sub pod time code
1             package Net::PublicSuffixList;
2 5     5   2163996 use v5.26;
  5         30  
3 5     5   23 use strict;
  5         8  
  5         165  
4 5     5   28 use feature qw(signatures);
  5         24  
  5         796  
5 5     5   35 no warnings qw(experimental::signatures);
  5         9  
  5         251  
6              
7 5     5   20 use warnings;
  5         8  
  5         207  
8 5     5   38 no warnings;
  5         11  
  5         155  
9              
10 5     5   20 use Carp qw(carp);
  5         10  
  5         368  
11 5     5   25 use File::Basename qw(basename dirname);
  5         14  
  5         299  
12 5     5   23 use File::Path qw(make_path);
  5         8  
  5         284  
13 5     5   904 use File::Spec::Functions qw(catfile);
  5         1338  
  5         11430  
14              
15             our $VERSION = '0.503';
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 43885 sub new ( $class, %args ) {
  11         22  
  11         36  
  11         18  
81 11         24 my $self = bless {}, $class;
82 11         33 $self->_init( \%args );
83             }
84              
85 11     11   21 sub _init ( $self, $args ) {
  11         19  
  11         15  
  11         19  
86 11         42 my %args = ( $self->defaults->%*, $args->%* );
87              
88 11         58 while( my($k, $v) = each %args ) {
89 55         125 $self->{$k} = $v;
90 55 100       169 if( $k eq 'local_path' ) {
91 11         539 $self->{local_file} = basename( $v );
92             }
93             }
94              
95 11         18 my $method = do {
96 11 100 66     48 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         1600 carp "No way to fetch list! Check your settings for no_local or no_net";
104 9         75 return $self;
105             }
106             };
107              
108 2         10 my $ref = $self->$method();
109              
110 2         11 $self->parse_list( $ref );
111              
112 2         13 $self;
113             }
114              
115             =item defaults
116              
117             A hash of the default values for everything.
118              
119             =cut
120              
121 11     11 1 27 sub defaults ( $self ) {
  11         20  
  11         17  
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         22 cache_dir => catfile( $ENV{HOME}, '.publicsuffixlist' ),
128             };
129 11         65 $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 11319 sub parse_list ( $self, $list ) {
  6         14  
  6         10  
  6         10  
140 6 100       25 unless( ref $list eq 'SCALAR' ) {
141 2         197 carp "Argument is not a scalar reference";
142 2         12 return;
143             }
144              
145 4         28 my( $line_ending ) = $$list =~ m/(\R)/;
146 4         60 open my $string_fh, '<:utf8', $list;
147 4         36 $string_fh->input_record_separator( $line_ending );
148              
149 4         801 while( <$string_fh> ) {
150 28         56 chomp;
151 28 100 100     163 next if( /\A\s*\z/ || m|\A\s*//| );
152 20         75 s/\A\Q*.//;
153 20         66 $self->add_suffix( $_ );
154             }
155 4         25 $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 1752 sub add_suffix ( $self, $suffix ) {
  26         43  
  26         41  
  26         41  
168 26         79 $suffix =~ s/\A[*.]+//;
169 26         79 $self->{suffix}{$suffix}++;
170 26         78 $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 2 sub remove_suffix ( $self, $suffix ) { delete $self->{suffix}{$suffix}; $self }
  1         3  
  1         2  
  1         2  
  1         2  
  1         1  
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 18195 sub suffix_exists ( $self, $suffix ) { exists $self->{suffix}{$suffix} ? $self : () }
  50         102  
  50         79  
  50         74  
  50         406  
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 4105 sub suffixes_in ( $self, $host ) {
  3         6  
  3         7  
  3         5  
198 3         15 my @parts = reverse split /\./, $host;
199             my @suffixes =
200 6         13 map { $_->[0] }
201 9         14 grep { $_->[1] }
202 9         16 map { [ $_, $self->suffix_exists( $_ ) ] }
203 3         10 map { join '.', reverse @parts[0..$_] }
  9         26  
204             0 .. $#parts;
205              
206 3         31 \@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 5221 sub longest_suffix_in ( $self, $host ) {
  2         6  
  2         4  
  2         4  
216 2         7 $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 3637 sub split_host ( $self, $host ) {
  1         3  
  1         3  
  1         2  
231 1         5 my $suffix = $self->longest_suffix_in( $host );
232 1         28 my $short = $host =~ s/\Q.$suffix\E\z//r;
233              
234             return {
235 1         6 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 912 sub fetch_list_from_local ( $self ) {
  3         5  
  3         4  
250 3 100       11 return if $self->{no_local};
251 2         31 open my $fh, '<:raw', $self->local_path;
252 2         7 my $data = do { local $/; <$fh> };
  2         11  
  2         184  
253 2         11 $self->{source} = 'local_file';
254 2         61 \$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 2 sub fetch_list_from_net ( $self ) {
  1         1  
  1         2  
271 1 50       12 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 1604 sub url ( $self ) {
  2         4  
  2         5  
324 2   66     12 $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 8 sub default_url ( $self ) {
  5         15  
  5         8  
334 5         18 '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 13 sub local_path ( $self ) {
  5         10  
  5         11  
344 5   33     160 $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 31 sub default_local_path ( $self ) {
  3         4  
  3         5  
354 3         5 my $this_file = __FILE__;
355 3         190 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 1185 sub local_file ( $self ) {
  1         3  
  1         3  
366 1   33     8 $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 6 sub default_local_file ( $self ) {
  3         4  
  3         7  
376 3         39 '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             L
389              
390             =head1 SOURCE AVAILABILITY
391              
392             This source is in Github:
393              
394             https://github.com/briandfoy/net-publicsuffixlist
395              
396             =head1 AUTHOR
397              
398             brian d foy, C<< >>
399              
400             =head1 COPYRIGHT AND LICENSE
401              
402             Copyright © 2020-2024, brian d foy, All Rights Reserved.
403              
404             You may redistribute this under the terms of the Artistic License 2.0.
405              
406             The public suffix list is Mozilla Public License 2.0
407              
408             =cut
409              
410             1;