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.19';
3 9     9   640464 use strict;
  9         94  
  9         320  
4 9     9   55 use warnings;
  9         19  
  9         352  
5 9     9   52 use base 'Class::Accessor::Fast';
  9         24  
  9         4869  
6              
7 9     9   37753 use Domain::PublicSuffix::Default ();
  9         25  
  9         212  
8 9     9   63 use File::Spec ();
  9         17  
  9         154  
9 9     9   4501 use Net::IDN::Encode ();
  9         1012225  
  9         14661  
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 15703 my ( $class, @args ) = @_;
129              
130 10         140 my $self = $class->SUPER::new(@args);
131              
132             # Compatibility fix
133 10 100 66     220 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 10         78 $self->tld_tree($self->_parse_data_to_tree());
138              
139 10         5335 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 14214 my ( $self, $inbound ) = @_;
156              
157 102 100       240 unless ($inbound) {
158 1         30 $self->error('No input');
159 1         12 return;
160             }
161              
162 101         262 my $domain = lc($inbound);
163              
164             # Clear meta properties
165 101         233 foreach ( qw/tld suffix root_domain error/ ) {
166 404         802 undef( $self->{$_} );
167             }
168              
169             # Check if domain is valid
170 101 100       252 unless ( _validate_domain($domain) ) {
171 15         387 $self->error('Malformed domain');
172 15         127 return;
173             }
174              
175 86         227 my @domain_array = split(/\./, $domain);
176 86         165 my $tld = pop(@domain_array);
177 86 100       2138 unless ( defined $self->tld_tree->{$tld} ) {
178 3 50       79 if ( $self->allow_unlisted_tld ) {
179 3         69 $self->tld($tld);
180 3         65 $self->suffix($tld);
181 3 50       22 if ( my $next = pop(@domain_array) ) {
182 3         59 $self->root_domain( join( '.', $next, $tld ) );
183             } else {
184 0         0 $self->root_domain($tld);
185             }
186 3         66 return $self->root_domain;
187             } else {
188 0         0 $self->error('Invalid TLD');
189 0         0 return;
190             }
191             }
192              
193 83         1971 $self->tld($tld);
194 83         604 my $raw_suffix = $self->get_suffix_for_domain($domain);
195              
196             # Leave if we still haven't found a suffix
197 83 50       189 if ( !$raw_suffix ) {
198 0         0 $self->error('Domain not valid');
199 0         0 return;
200             }
201              
202 83         132 my $suffix = $raw_suffix;
203 83         163 $suffix =~ s/!//g;
204 83         1540 $self->suffix($suffix);
205              
206             # Check if we're left with just a suffix
207 83 100 100     1824 if ( $raw_suffix !~ /!/ and $self->suffix eq $domain ) {
208 14         331 $self->error('Domain is already a suffix');
209 14         124 return;
210             }
211              
212             # Generate root domain using suffix
213 69 100       516 if ($raw_suffix =~ /!/) {
214             # Exception suffixes are also domains
215 6         111 $self->root_domain($suffix);
216             } else {
217 63         103 my $root_domain = $domain;
218 63         1173 $root_domain =~ s/^.*\.(.*?\.$suffix)$/$1/;
219 63         1231 $self->root_domain($root_domain);
220             }
221              
222 69         1535 return $self->root_domain;
223             }
224              
225             sub get_suffix_for_domain {
226 83     83 0 172 my ( $self, $domain ) = @_;
227              
228 83         224 my @labels = split( /\./, $domain );
229 83         1438 my $point = $self->tld_tree;
230 83         363 my @suffix;
231 83         217 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       732 if ( $point->{'*'} ) {
    100          
237 16         39 my $exception = '!' . $label;
238 16 100       56 if ( $point->{$exception} ) {
239 6         14 push( @suffix, $exception );
240 6         15 last;
241             }
242             } elsif (!$point->{$label}) {
243             # If we run out of rules at this point, the root is just below here
244 62         119 last;
245             }
246            
247 146         285 push( @suffix, $label );
248 146         370 $point = $point->{$label};
249             }
250 83         311 return join('.', reverse(@suffix));
251             }
252              
253             sub _load_data {
254 10     10   77 my ($self) = @_;
255              
256 10         40 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     270 if ( $self->use_default ) {
    50          
262 7         118 $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         362 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         21 foreach my $path (@paths) {
279 15         161 $path = File::Spec->catfile( $path, "effective_tld_names.dat" );
280 15 50       411 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       73 unless ( defined $data_stream_ref ) {
293 3         17 $data_stream_ref = Domain::PublicSuffix::Default::retrieve();
294             }
295              
296 10         41 return $data_stream_ref;
297             }
298              
299             sub _parse_data_to_tree {
300 10     10   44 my ($self) = @_;
301              
302 10         130 my $data_stream_ref = $self->_load_data();
303 10         24 my $tree = {};
304              
305 10         24 foreach (@{$data_stream_ref}) {
  10         37  
306 135870         233808 chomp;
307            
308             # Remove comments, skip if full line comment, remove if inline comment
309 135870 100 100     488737 next if ( /^\// or /^[ \t]*?$/ );
310 91280         174714 s/[\s\x{0085}\x{000A}\x{000C}\x{000D}\x{0020}].*//;
311              
312             # Parse both unicode and ASCII representations, if needed
313 91280         171287 my @tlds = ($_);
314 91280         185520 my $ascii = Net::IDN::Encode::domain_to_ascii($_);
315 91280 100       5558325 push( @tlds, $ascii ) if ( $_ ne $ascii );
316              
317 91280         157725 foreach (@tlds) {
318             # Split domain and convert to a tree
319 95910         255291 my @domain = split( /\./, $_ );
320 95910         141861 my $previous = $tree;
321 95910         197081 while (my $label = pop(@domain)) {
322 200620         331278 $label =~ s/\s.*//;
323 200620   100     721575 $previous->{$label} ||= {};
324 200620         539184 $previous = $previous->{$label};
325             }
326             }
327             }
328 10         14157 return $tree;
329             }
330              
331             sub _validate_domain {
332 101     101   198 my ($domain) = @_;
333              
334 101   66     219 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   184 my ($domain) = @_;
340              
341 101         351 my @segments = split( /\./, $domain );
342 101 100       277 return unless ( @segments > 1 );
343 90         188 foreach my $segment (@segments) {
344 266 100       519 return unless ( length($segment) > 0 );
345             }
346 86         332 return 1;
347             }
348              
349             # Domains may not be more than 255 characters in length
350             sub _validate_length {
351 101     101   181 my ($domain) = @_;
352              
353 101         176 my $length = length($domain);
354 101   33     576 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;