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.17';
3 9     9   519538 use strict;
  9         82  
  9         232  
4 9     9   39 use warnings;
  9         15  
  9         265  
5 9     9   53 use base 'Class::Accessor::Fast';
  9         19  
  9         3918  
6              
7 9     9   29898 use Domain::PublicSuffix::Default ();
  9         21  
  9         172  
8 9     9   54 use File::Spec ();
  9         14  
  9         118  
9 9     9   3591 use Net::IDN::Encode ();
  9         830167  
  9         11799  
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 10     10 1 12267 my ( $class, @args ) = @_;
129              
130 10         126 my $self = $class->SUPER::new(@args);
131              
132             # Compatibility fix
133 10 100 66     179 if ( $args[0] and ref($args[0]) eq 'HASH' and $args[0]->{'dataFile'} ) {
      100        
134 1         21 $self->data_file( $args[0]->{'dataFile'} );
135             }
136              
137 10         68 $self->tld_tree($self->_parse_data_to_tree());
138              
139 10         4308 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 102     102 1 11328 my ( $self, $inbound ) = @_;
156              
157 102 100       206 unless ($inbound) {
158 1         24 $self->error('No input');
159 1         10 return;
160             }
161              
162 101         244 my $domain = lc($inbound);
163              
164             # Clear meta properties
165 101         189 foreach ( qw/tld suffix root_domain error/ ) {
166 404         633 undef( $self->{$_} );
167             }
168              
169             # Check if domain is valid
170 101 100       205 unless ( _validate_domain($domain) ) {
171 15         309 $self->error('Malformed domain');
172 15         101 return;
173             }
174              
175 86         181 my @domain_array = split(/\./, $domain);
176 86         133 my $tld = pop(@domain_array);
177 86 100       1750 unless ( defined $self->tld_tree->{$tld} ) {
178 3 50       62 if ( $self->allow_unlisted_tld ) {
179 3         54 $self->tld($tld);
180 3         51 $self->suffix($tld);
181 3 50       17 if ( my $next = pop(@domain_array) ) {
182 3         44 $self->root_domain( join( '.', $next, $tld ) );
183             } else {
184 0         0 $self->root_domain($tld);
185             }
186 3         52 return $self->root_domain;
187             } else {
188 0         0 $self->error('Invalid TLD');
189 0         0 return;
190             }
191             }
192              
193 83         1550 $self->tld($tld);
194 83         494 my $raw_suffix = $self->get_suffix_for_domain($domain);
195              
196             # Leave if we still haven't found a suffix
197 83 50       158 if ( !$raw_suffix ) {
198 0         0 $self->error('Domain not valid');
199 0         0 return;
200             }
201              
202 83         118 my $suffix = $raw_suffix;
203 83         129 $suffix =~ s/!//g;
204 83         1269 $self->suffix($suffix);
205              
206             # Check if we're left with just a suffix
207 83 100 100     1430 if ( $raw_suffix !~ /!/ and $self->suffix eq $domain ) {
208 14         260 $self->error('Domain is already a suffix');
209 14         100 return;
210             }
211              
212             # Generate root domain using suffix
213 69 100       408 if ($raw_suffix =~ /!/) {
214             # Exception suffixes are also domains
215 6         83 $self->root_domain($suffix);
216             } else {
217 63         83 my $root_domain = $domain;
218 63         931 $root_domain =~ s/^.*\.(.*?\.$suffix)$/$1/;
219 63         997 $self->root_domain($root_domain);
220             }
221              
222 69         1210 return $self->root_domain;
223             }
224              
225             sub get_suffix_for_domain {
226 83     83 0 140 my ( $self, $domain ) = @_;
227              
228 83         183 my @labels = split( /\./, $domain );
229 83         1122 my $point = $self->tld_tree;
230 83         293 my @suffix;
231 83         176 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 214 100       544 if ( $point->{'*'} ) {
    100          
237 16         28 my $exception = '!' . $label;
238 16 100       37 if ( $point->{$exception} ) {
239 6         11 push( @suffix, $exception );
240 6         9 last;
241             }
242             } elsif (!$point->{$label}) {
243             # If we run out of rules at this point, the root is just below here
244 62         91 last;
245             }
246            
247 146         234 push( @suffix, $label );
248 146         295 $point = $point->{$label};
249             }
250 83         266 return join('.', reverse(@suffix));
251             }
252              
253             sub _load_data {
254 10     10   27 my ($self) = @_;
255              
256 10         28 my $data_stream_ref;
257              
258             # Find an effective_tld_names.dat file
259             my @tld_lines;
260 10         0 my $dat;
261 10 100 66     219 if ( $self->use_default ) {
    50          
262 7         86 $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         254 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         12 foreach my $path (@paths) {
279 15         110 $path = File::Spec->catfile( $path, "effective_tld_names.dat" );
280 15 50       315 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 10 100       43 unless ( defined $data_stream_ref ) {
293 3         15 $data_stream_ref = Domain::PublicSuffix::Default::retrieve();
294             }
295              
296 10         28 return $data_stream_ref;
297             }
298              
299             sub _parse_data_to_tree {
300 10     10   35 my ($self) = @_;
301              
302 10         118 my $data_stream_ref = $self->_load_data();
303 10         23 my $tree = {};
304              
305 10         19 foreach (@{$data_stream_ref}) {
  10         33  
306 131350         179294 chomp;
307            
308             # Remove comments, skip if full line comment, remove if inline comment
309 131350 100 100     382080 next if ( /^\// or /^[ \t]*?$/ );
310 88500         136005 s/[\s\x{0085}\x{000A}\x{000C}\x{000D}\x{0020}].*//;
311              
312             # Parse both unicode and ASCII representations, if needed
313 88500         135964 my @tlds = ($_);
314 88500         146081 my $ascii = Net::IDN::Encode::domain_to_ascii($_);
315 88500 100       4382193 push( @tlds, $ascii ) if ( $_ ne $ascii );
316              
317 88500         127884 foreach (@tlds) {
318             # Split domain and convert to a tree
319 93010         200193 my @domain = split( /\./, $_ );
320 93010         109644 my $previous = $tree;
321 93010         154745 while (my $label = pop(@domain)) {
322 193430         259580 $label =~ s/\s.*//;
323 193430   100     543882 $previous->{$label} ||= {};
324 193430         419511 $previous = $previous->{$label};
325             }
326             }
327             }
328 10         11148 return $tree;
329             }
330              
331             sub _validate_domain {
332 101     101   153 my ($domain) = @_;
333              
334 101   66     181 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 101     101   188 my ($domain) = @_;
340              
341 101         294 my @segments = split( /\./, $domain );
342 101 100       236 return unless ( @segments > 1 );
343 90         154 foreach my $segment (@segments) {
344 266 100       425 return unless ( length($segment) > 0 );
345             }
346 86         268 return 1;
347             }
348              
349             # Domains may not be more than 255 characters in length
350             sub _validate_length {
351 101     101   146 my ($domain) = @_;
352              
353 101         145 my $length = length($domain);
354 101   33     486 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 8 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;