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.21';
3 10     10   1151156 use strict;
  10         20  
  10         377  
4 10     10   48 use warnings;
  10         29  
  10         694  
5 10     10   74 use base 'Class::Accessor::Fast';
  10         22  
  10         5299  
6              
7 10     10   46679 use Domain::PublicSuffix::Default ();
  10         50  
  10         404  
8 10     10   136 use File::Spec ();
  10         19  
  10         240  
9 10     10   5766 use Net::IDN::Encode ();
  10         805812  
  10         16790  
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 1601202 my ( $class, @args ) = @_;
129              
130 11         167 my $self = $class->SUPER::new(@args);
131              
132             # Compatibility fix
133 11 100 66     265 if ( $args[0] and ref($args[0]) eq 'HASH' and $args[0]->{'dataFile'} ) {
      100        
134 1         28 $self->data_file( $args[0]->{'dataFile'} );
135             }
136              
137 11         71 $self->tld_tree($self->_parse_data_to_tree());
138              
139 11         5000 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 20364 my ( $self, $inbound ) = @_;
156              
157 103 100       344 unless ($inbound) {
158 1         39 $self->error('No input');
159 1         11 return;
160             }
161              
162 102         290 my $domain = lc($inbound);
163              
164             # Clear meta properties
165 102         259 foreach ( qw/tld suffix root_domain error/ ) {
166 408         1075 undef( $self->{$_} );
167             }
168              
169             # Check if domain is valid
170 102 100       323 unless ( _validate_domain($domain) ) {
171 15         555 $self->error('Malformed domain');
172 15         160 return;
173             }
174              
175 87         275 my @domain_array = split(/\./, $domain);
176 87         198 my $tld = pop(@domain_array);
177 87 100       3634 unless ( defined $self->tld_tree->{$tld} ) {
178 3 50       113 if ( $self->allow_unlisted_tld ) {
179 3         87 $self->tld($tld);
180 3         86 $self->suffix($tld);
181 3 50       25 if ( my $next = pop(@domain_array) ) {
182 3         75 $self->root_domain( join( '.', $next, $tld ) );
183             } else {
184 0         0 $self->root_domain($tld);
185             }
186 3         117 return $self->root_domain;
187             } else {
188 0         0 $self->error('Invalid TLD');
189 0         0 return;
190             }
191             }
192              
193 84         3127 $self->tld($tld);
194 84         780 my $raw_suffix = $self->get_suffix_for_domain($domain);
195              
196             # Leave if we still haven't found a suffix
197 84 50       235 if ( !$raw_suffix ) {
198 0         0 $self->error('Domain not valid');
199 0         0 return;
200             }
201              
202 84         159 my $suffix = $raw_suffix;
203 84         205 $suffix =~ s/!//g;
204 84         2171 $self->suffix($suffix);
205              
206             # Check if we're left with just a suffix
207 84 100 100     2421 if ( $raw_suffix !~ /!/ and $self->suffix eq $domain ) {
208 14         401 $self->error('Domain is already a suffix');
209 14         147 return;
210             }
211              
212             # Generate root domain using suffix
213 70 100       550 if ($raw_suffix =~ /!/) {
214             # Exception suffixes are also domains
215 6         148 $self->root_domain($suffix);
216             } else {
217 64         150 my $root_domain = $domain;
218 64         1732 $root_domain =~ s/^.*\.(.*?\.$suffix)\.?$/$1/;
219 64         1926 $self->root_domain($root_domain);
220             }
221              
222 70         1988 return $self->root_domain;
223             }
224              
225             sub get_suffix_for_domain {
226 84     84 0 226 my ( $self, $domain ) = @_;
227              
228 84         273 my @labels = split( /\./, $domain );
229 84         1960 my $point = $self->tld_tree;
230 84         508 my @suffix;
231 84         297 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       827 if ( $point->{'*'} ) {
    100          
237 16         34 my $exception = '!' . $label;
238 16 100       47 if ( $point->{$exception} ) {
239 6         12 push( @suffix, $exception );
240 6         14 last;
241             }
242             } elsif (!$point->{$label}) {
243             # If we run out of rules at this point, the root is just below here
244 63         151 last;
245             }
246            
247 147         354 push( @suffix, $label );
248 147         490 $point = $point->{$label};
249             }
250 84         398 return join('.', reverse(@suffix));
251             }
252              
253             sub _load_data {
254 11     11   29 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     351 if ( $self->use_default ) {
    50          
262 8         124 $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         303 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         18 foreach my $path (@paths) {
279 15         87 $path = File::Spec->catfile( $path, "effective_tld_names.dat" );
280 15 50       615 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       74 unless ( defined $data_stream_ref ) {
293 3         16 $data_stream_ref = Domain::PublicSuffix::Default::retrieve();
294             }
295              
296 11         46 return $data_stream_ref;
297             }
298              
299             sub _parse_data_to_tree {
300 11     11   36 my ($self) = @_;
301              
302 11         50 my $data_stream_ref = $self->_load_data();
303 11         30 my $tree = {};
304              
305 11         49 foreach (@{$data_stream_ref}) {
  11         54  
306 173250         285508 chomp;
307            
308             # Remove comments, skip if full line comment, remove if inline comment
309 173250 100 100     628698 next if ( /^\// or /^[ \t]*?$/ );
310 107459         222676 s/[\s\x{0085}\x{000A}\x{000C}\x{000D}\x{0020}].*//;
311              
312             # Parse both unicode and ASCII representations, if needed
313 107459         208119 my @tlds = ($_);
314 107459         230662 my $ascii = Net::IDN::Encode::domain_to_ascii($_);
315 107459 100       6836492 push( @tlds, $ascii ) if ( $_ ne $ascii );
316              
317 107459         199072 foreach (@tlds) {
318             # Split domain and convert to a tree
319 112519         320467 my @domain = split( /\./, $_ );
320 112519         166372 my $previous = $tree;
321 112519         246301 while (my $label = pop(@domain)) {
322 253682         441616 $label =~ s/\s.*//;
323 253682   100     933651 $previous->{$label} ||= {};
324 253682         716789 $previous = $previous->{$label};
325             }
326             }
327             }
328 11         16128 return $tree;
329             }
330              
331             sub _validate_domain {
332 102     102   249 my ($domain) = @_;
333              
334 102   66     291 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   233 my ($domain) = @_;
340              
341 102         391 my @segments = split( /\./, $domain );
342 102 100       384 return unless ( @segments > 1 );
343 91         212 foreach my $segment (@segments) {
344 269 100       725 return unless ( length($segment) > 0 );
345             }
346 87         412 return 1;
347             }
348              
349             # Domains may not be more than 255 characters in length
350             sub _validate_length {
351 102     102   212 my ($domain) = @_;
352              
353 102         250 my $length = length($domain);
354 102   33     790 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         5 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;