File Coverage

blib/lib/Domain/PublicSuffix.pm
Criterion Covered Total %
statement 116 138 84.0
branch 35 44 79.5
condition 18 23 78.2
subroutine 15 19 78.9
pod 2 6 33.3
total 186 230 80.8


line stmt bran cond sub pod time code
1             package Domain::PublicSuffix;
2             $Domain::PublicSuffix::VERSION = '0.20';
3 10     10   694968 use strict;
  10         106  
  10         298  
4 10     10   53 use warnings;
  10         20  
  10         274  
5 10     10   51 use base 'Class::Accessor::Fast';
  10         19  
  10         5070  
6              
7 10     10   43353 use Domain::PublicSuffix::Default ();
  10         30  
  10         230  
8 10     10   69 use File::Spec ();
  10         20  
  10         136  
9 10     10   4753 use Net::IDN::Encode ();
  10         1123303  
  10         16809  
10              
11             __PACKAGE__->mk_accessors(qw/
12             use_default
13             data_file
14             allow_unlisted_tld
15             domain_allow_underscore
16             tld_tree
17             error
18             root_domain
19             tld
20             suffix
21             /);
22              
23             =head1 NAME
24              
25             Domain::PublicSuffix - Parse a domain down to root
26              
27             =head1 SYNOPSIS
28              
29             use Domain::PublicSuffix;
30              
31             my $suffix = Domain::PublicSuffix->new({
32             'data_file' => '/tmp/effective_tld_names.dat'
33             });
34             my $root = $suffix->get_root_domain('www.google.com');
35             # $root now contains "google.com"
36              
37             $root = $suffix->get_root_domain('www.google.co.uk');
38             # $root now contains google.co.uk
39              
40             my $suf = $suffix->suffix();
41             # $suf now contains co.uk
42              
43             my $tld = $suffix->tld();
44             # $tld now contains uk
45              
46             =head1 DESCRIPTION
47              
48             This module utilizes the "effective_tld_names.dat" provided by Mozilla as a way
49             to effectively reduce a fully qualified domain name down to the absolute root.
50             The Mozilla PublicSuffix file is an open source, fully documented format that
51             shows absolute root TLDs, primarily for Mozilla's browser products to be able
52             to determine how far a cookie's security boundaries go.
53              
54             This module will attempt to search etc directories in /usr/share/publicsuffix,
55             /usr, /usr/local, and /opt/local for the effective_tld_names.dat file. If a file
56             is not found, a default file is loaded from Domain::PublicSuffix::Default, which
57             is current at the time of the module's release. You can override the data file
58             path by giving the new() method a 'data_file' argument.
59              
60             When creating a new PublicSuffix object, the module will load the data file as
61             specified, and use the internal structure to parse each domain sent to the
62             get_root_domain method. To re-parse the file, you must destroy and create a new
63             object, or execute the _parse_data_file method directly, though that is not
64             recommended.
65              
66             =head1 PUBLIC ACCESSORS
67              
68             =over 4
69              
70             =item error()
71              
72             On unsuccessful parse, contains a human-readable error string.
73              
74             =item suffix()
75              
76             Returns the effective tld of the last parsed domain. For the domain
77             'google.co.uk', this would return 'co.uk'.
78              
79             =item tld()
80              
81             Returns the true DNS tld of the last parsed domain. For the domain
82             'google.co.uk', this would return 'uk'.
83              
84             =back
85              
86             =cut
87              
88             =head1 PUBLIC METHODS
89              
90             =over 4
91              
92             =item new(\%arguments)
93              
94             Instantiate a PublicSuffix object. It is best to instantiate an object
95             and continue calling get_root_domain instead of continually recreating the
96             object, as the data file is read and parsed on instantiation.
97              
98             Can take a hashref of arguments:
99              
100             =over 4
101              
102             =item data_file
103              
104             A fully qualified path, to override the effective_tld_names.dat file.
105              
106             =item use_default
107              
108             Use the provided publicsuffix file, do not search for any other files.
109              
110             =item domain_allow_underscore
111              
112             A flag to indicate that underscores should be allowed in hostnames
113             (contra to the RFCs). Default: undef.
114              
115             =item allow_unlisted_tld
116              
117             A flag to indicate that unlisted TLDs should be passed through. This follows
118             the spec as listed on publicsuffix.org, but is not how this module works by
119             default, or before 0.16. Default: undef
120              
121             =back
122              
123             =back
124              
125             =cut
126              
127             sub new {
128 11     11 1 15215 my ( $class, @args ) = @_;
129              
130 11         151 my $self = $class->SUPER::new(@args);
131              
132             # Compatibility fix
133 11 100 66     241 if ( $args[0] and ref($args[0]) eq 'HASH' and $args[0]->{'dataFile'} ) {
      100        
134 1         25 $self->data_file( $args[0]->{'dataFile'} );
135             }
136              
137 11         59 $self->tld_tree($self->_parse_data_to_tree());
138              
139 11         5457 return $self;
140             }
141              
142             =over 4
143              
144             =item get_root_domain( $domain )
145              
146             Given a fully qualified domain name, return the parsed root domain name.
147             Returns undefined if an error occurs parsing the given domain, and fills
148             the error accessor with a human-readable error string.
149              
150             =back
151              
152             =cut
153              
154             sub get_root_domain {
155 103     103 1 14475 my ( $self, $inbound ) = @_;
156              
157 103 100       243 unless ($inbound) {
158 1         30 $self->error('No input');
159 1         14 return;
160             }
161              
162 102         258 my $domain = lc($inbound);
163              
164             # Clear meta properties
165 102         215 foreach ( qw/tld suffix root_domain error/ ) {
166 408         767 undef( $self->{$_} );
167             }
168              
169             # Check if domain is valid
170 102 100       225 unless ( _validate_domain($domain) ) {
171 15         399 $self->error('Malformed domain');
172 15         132 return;
173             }
174              
175 87         217 my @domain_array = split(/\./, $domain);
176 87         176 my $tld = pop(@domain_array);
177 87 100       2231 unless ( defined $self->tld_tree->{$tld} ) {
178 3 50       85 if ( $self->allow_unlisted_tld ) {
179 3         79 $self->tld($tld);
180 3         72 $self->suffix($tld);
181 3 50       20 if ( my $next = pop(@domain_array) ) {
182 3         58 $self->root_domain( join( '.', $next, $tld ) );
183             } else {
184 0         0 $self->root_domain($tld);
185             }
186 3         65 return $self->root_domain;
187             } else {
188 0         0 $self->error('Invalid TLD');
189 0         0 return;
190             }
191             }
192              
193 84         2011 $self->tld($tld);
194 84         621 my $raw_suffix = $self->get_suffix_for_domain($domain);
195              
196             # Leave if we still haven't found a suffix
197 84 50       210 if ( !$raw_suffix ) {
198 0         0 $self->error('Domain not valid');
199 0         0 return;
200             }
201              
202 84         132 my $suffix = $raw_suffix;
203 84         167 $suffix =~ s/!//g;
204 84         1615 $self->suffix($suffix);
205              
206             # Check if we're left with just a suffix
207 84 100 100     1865 if ( $raw_suffix !~ /!/ and $self->suffix eq $domain ) {
208 14         330 $self->error('Domain is already a suffix');
209 14         123 return;
210             }
211              
212             # Generate root domain using suffix
213 70 100       535 if ($raw_suffix =~ /!/) {
214             # Exception suffixes are also domains
215 6         109 $self->root_domain($suffix);
216             } else {
217 64         107 my $root_domain = $domain;
218 64         1216 $root_domain =~ s/^.*\.(.*?\.$suffix)\.?$/$1/;
219 64         1360 $self->root_domain($root_domain);
220             }
221              
222 70         1587 return $self->root_domain;
223             }
224              
225             sub get_suffix_for_domain {
226 84     84 0 179 my ( $self, $domain ) = @_;
227              
228 84         228 my @labels = split( /\./, $domain );
229 84         1436 my $point = $self->tld_tree;
230 84         383 my @suffix;
231 84         226 while ( my $label = pop(@labels) ) {
232             # If there is a wildcard here, it is a suffix, except for !exceptions
233             # Theoretically, there would be further processing here for .*.
234             # wildcards, but those have not existed before in the list, so saving
235             # the work until it actually happens.
236 216 100       669 if ( $point->{'*'} ) {
    100          
237 16         35 my $exception = '!' . $label;
238 16 100       46 if ( $point->{$exception} ) {
239 6         11 push( @suffix, $exception );
240 6         47 last;
241             }
242             } elsif (!$point->{$label}) {
243             # If we run out of rules at this point, the root is just below here
244 63         124 last;
245             }
246            
247 147         303 push( @suffix, $label );
248 147         356 $point = $point->{$label};
249             }
250 84         348 return join('.', reverse(@suffix));
251             }
252              
253             sub _load_data {
254 11     11   36 my ($self) = @_;
255              
256 11         40 my $data_stream_ref;
257              
258             # Find an effective_tld_names.dat file
259             my @tld_lines;
260 11         0 my $dat;
261 11 100 66     283 if ( $self->use_default ) {
    50          
262 8         130 $data_stream_ref = Domain::PublicSuffix::Default::retrieve();
263             } elsif ( $self->data_file and -e $self->data_file ) {
264 0 0       0 open( $dat, '<:encoding(UTF-8)', $self->data_file )
265             or die "Cannot open \'" . $self->data_file . "\': " . $!;
266 0         0 @tld_lines = <$dat>;
267 0         0 close($dat);
268 0         0 $data_stream_ref = \@tld_lines;
269              
270             } else {
271 3         324 my @paths = (
272             File::Spec->catdir(File::Spec->rootdir, qw/ usr share publicsuffix /),
273             File::Spec->catdir(File::Spec->rootdir, qw/ etc /),
274             File::Spec->catdir(File::Spec->rootdir, qw/ usr etc /),
275             File::Spec->catdir(File::Spec->rootdir, qw/ usr local etc /),
276             File::Spec->catdir(File::Spec->rootdir, qw/ opt local etc /),
277             );
278 3         15 foreach my $path (@paths) {
279 15         131 $path = File::Spec->catfile( $path, "effective_tld_names.dat" );
280 15 50       420 if ( -e $path ) {
281 0 0       0 open( $dat, '<:encoding(UTF-8)', $path )
282             or die "Cannot open \'" . $path . "\': " . $!;
283 0         0 @tld_lines = <$dat>;
284 0         0 close($dat);
285 0         0 $data_stream_ref = \@tld_lines;
286 0         0 last;
287             }
288             }
289             }
290              
291             # If we haven't found one, load the default
292 11 100       63 unless ( defined $data_stream_ref ) {
293 3         16 $data_stream_ref = Domain::PublicSuffix::Default::retrieve();
294             }
295              
296 11         41 return $data_stream_ref;
297             }
298              
299             sub _parse_data_to_tree {
300 11     11   47 my ($self) = @_;
301              
302 11         138 my $data_stream_ref = $self->_load_data();
303 11         32 my $tree = {};
304              
305 11         22 foreach (@{$data_stream_ref}) {
  11         42  
306 164472         285264 chomp;
307            
308             # Remove comments, skip if full line comment, remove if inline comment
309 164472 100 100     574411 next if ( /^\// or /^[ \t]*?$/ );
310 100144         194362 s/[\s\x{0085}\x{000A}\x{000C}\x{000D}\x{0020}].*//;
311              
312             # Parse both unicode and ASCII representations, if needed
313 100144         191458 my @tlds = ($_);
314 100144         207362 my $ascii = Net::IDN::Encode::domain_to_ascii($_);
315 100144 100       6138165 push( @tlds, $ascii ) if ( $_ ne $ascii );
316              
317 100144         173424 foreach (@tlds) {
318             # Split domain and convert to a tree
319 105215         270558 my @domain = split( /\./, $_ );
320 105215         149521 my $previous = $tree;
321 105215         213877 while (my $label = pop(@domain)) {
322 224301         369541 $label =~ s/\s.*//;
323 224301   100     803263 $previous->{$label} ||= {};
324 224301         594560 $previous = $previous->{$label};
325             }
326             }
327             }
328 11         16125 return $tree;
329             }
330              
331             sub _validate_domain {
332 102     102   206 my ($domain) = @_;
333              
334 102   66     215 return ( _validate_length($domain) and _validate_multiple_segments($domain) );
335             }
336              
337             # Domains must have more than one segment with length
338             sub _validate_multiple_segments {
339 102     102   195 my ($domain) = @_;
340              
341 102         352 my @segments = split( /\./, $domain );
342 102 100       295 return unless ( @segments > 1 );
343 91         186 foreach my $segment (@segments) {
344 269 100       521 return unless ( length($segment) > 0 );
345             }
346 87         329 return 1;
347             }
348              
349             # Domains may not be more than 255 characters in length
350             sub _validate_length {
351 102     102   174 my ($domain) = @_;
352              
353 102         196 my $length = length($domain);
354 102   33     564 return ( $length > 1 and $length <= 255 );
355             }
356              
357             ### Compatibility
358              
359             sub _parseDataFile {
360 0     0   0 my ($self) = @_;
361              
362 0         0 $self->tld_tree($self->_parse_data_to_tree());
363             }
364             sub getRootDomain {
365 1     1 0 11 my ( $self, $domain ) = @_;
366              
367 1         6 return $self->get_root_domain($domain);
368             }
369              
370             sub _validateDomain {
371 0     0     my ($self, $domain) = @_;
372              
373 0           return $self->_validate_domain($domain);
374             }
375              
376             sub dataFile {
377 0     0 0   my ( $self, $data_file ) = @_;
378              
379 0           return $self->data_file($data_file);
380             }
381              
382             sub rootDomain {
383 0     0 0   my ( $self, $root_domain ) = @_;
384              
385 0           return $self->root_domain($root_domain);
386             }
387              
388             =head1 SEE ALSO
389              
390             =over 4
391              
392             =item * GitHub
393              
394             L
395              
396             =item * Current List:
397              
398             L [mxr.mozilla.org]
399              
400             =item * Mozilla Documentation:
401              
402             L
403              
404             =item * Public Info Site:
405              
406             L
407              
408             =back
409              
410             =head1 BUGS
411              
412             Please report any bugs or feature requests to C,
413             or through the web interface at L.
414             I will be notified, and then you'll automatically be notified of progress on
415             your bug as I make changes.
416              
417             =head1 SUPPORT
418              
419             You can find documentation for this module with the perldoc command.
420              
421             perldoc Domain::PublicSuffix
422              
423             You can also look for information at:
424              
425             =over 4
426              
427             =item * RT: CPAN's request tracker
428              
429             L
430              
431             =item * AnnoCPAN: Annotated CPAN documentation
432              
433             L
434              
435             =item * CPAN Ratings
436              
437             L
438              
439             =item * Search CPAN
440              
441             L
442              
443             =back
444              
445             =head1 CONTRIBUTORS
446              
447             dkg: Daniel Kahn Gillmor
448              
449             gavinc: Gavin Carr
450              
451             jwieland: Jason Wieland
452              
453             =head1 COPYRIGHT & LICENSE
454              
455             Copyright 2008-2020 Nicholas Melnick, C.
456              
457             This program is free software; you can redistribute it and/or modify it
458             under the same terms as Perl itself.
459              
460             =cut
461              
462             1;