File Coverage

blib/lib/Libravatar/URL.pm
Criterion Covered Total %
statement 95 103 92.2
branch 47 54 87.0
condition 28 54 51.8
subroutine 14 14 100.0
pod 1 8 12.5
total 185 233 79.4


line stmt bran cond sub pod time code
1             package Libravatar::URL;
2              
3 2     2   165602 use strict;
  2         7  
  2         88  
4 2     2   23 use warnings;
  2         4  
  2         76  
5              
6 2     2   453 use Gravatar::URL qw(gravatar_url);
  2         3  
  2         148  
7 2     2   1989 use Digest::SHA qw(sha256_hex);
  2         21243  
  2         222  
8 2     2   22 use Carp;
  2         4  
  2         249  
9              
10             our $VERSION = '1.06';
11              
12 2     2   15 use parent 'Exporter';
  2         3  
  2         18  
13             our @EXPORT = qw(
14             libravatar_url
15             );
16              
17             my $Libravatar_Http_Base = "http://cdn.libravatar.org/avatar";
18             my $Libravatar_Https_Base = "https://seccdn.libravatar.org/avatar";
19              
20             =head1 NAME
21              
22             Libravatar::URL - Make URLs for Libravatars from an email address
23              
24             =head1 SYNOPSIS
25              
26             use Libravatar::URL;
27              
28             my $url = libravatar_url( email => 'larry@example.org' );
29              
30             =head1 DESCRIPTION
31              
32             See L for more information.
33              
34             =head1 Functions
35              
36             =head3 B
37              
38             # By email
39             my $url = libravatar_url( email => $email, %options );
40              
41             # By OpenID
42             my $url = libravatar_url( openid => $openid, %options );
43              
44             Constructs a URL to fetch the Libravatar for the given $email address or $openid URL.
45              
46             C<%options> are optional. C will accept all the
47             options of L except for C and C.
48              
49             The available options are...
50              
51             =head4 size
52              
53             Specifies the desired width and height of the avatar (they are square).
54              
55             Valid values are from 1 to 512 inclusive. Any size other than 80 may
56             cause the original image to be downsampled using bicubic resampling
57             before output.
58              
59             size => 40, # 40 x 40 image
60              
61             =head4 default
62              
63             The url to use if the user has no avatar.
64              
65             default => "http://www.example.org/nobody.jpg"
66              
67             Relative URLs will be relative to the base (ie. libravatar.org), not your web site.
68              
69             Libravatar defines special values that you may use as a default to
70             produce dynamic default images. These are "identicon", "monsterid",
71             "wavatar" and "retro". "404" will cause the URL to return an HTTP 404 "Not Found"
72             error instead and "mm" will display the same "mystery man" image for everybody.
73             See L for more info.
74              
75             If omitted, Libravatar will serve up their default image, the orange butterfly.
76              
77             =head4 base
78              
79             This is the URL of the location of the Libravatar server you wish to
80             grab avatars from. Defaults to
81             L for HTTP and
82             L for HTTPS.
83              
84             =head4 short_keys
85              
86             If true, use short key names when constructing the URL. "s" instead
87             of "size", "d" instead of "default" and so on.
88              
89             short_keys defaults to true.
90              
91             =head4 https
92              
93             If true, serve avatars over HTTPS instead of HTTP.
94              
95             You should select this option if your site is served over HTTPS to
96             avoid browser warnings about the presence of insecure content.
97              
98             https defaults to false.
99              
100             =cut
101              
102             my %defaults = (
103             short_keys => 1,
104             );
105              
106             # Extract the domain component of an email address
107             sub email_domain {
108 21     21 0 2287 my ( $email ) = @_;
109 21 100       60 return undef unless $email;
110              
111 20 100       141 if ( $email =~ m/@([^@]+)$/ ) {
112 19         87 return $1;
113             }
114 1         3 return undef;
115             }
116              
117             # Extract the domain component of an OpenID URI
118             sub openid_domain {
119 7     7 0 2690 my ( $openid ) = @_;
120 7 100       23 return undef unless $openid;
121              
122 6 100       30 if ( $openid =~ m@^(http|https)://([^/]+)@i ) {
123 5         19 return $2;
124             }
125 1         4 return undef;
126             }
127              
128             # Return the right (target, port) pair from a list of SRV records
129             sub srv_hostname {
130 8     8 0 11106 my @records = @_;
131 8 100       26 return ( undef, undef ) unless scalar(@records) > 0;
132              
133 7 100       18 if ( 1 == scalar(@records) ) {
134 1         3 my $rr = shift @records;
135 1         4 return ( $rr->target, $rr->port );
136             }
137              
138             # Keep only the servers in the top priority
139 6         8 my @priority_records;
140 6         8 my $total_weight = 0;
141 6         19 my $top_priority = $records[0]->priority; # highest priority = lowest number
142              
143 6         37 foreach my $rr (@records) {
144 25 100       58 if ( $rr->priority > $top_priority ) {
    100          
145             # ignore the record ($rr has lower priority)
146 6         38 next;
147             }
148             elsif ( $rr->priority < $top_priority ) {
149             # reset the array ($rr has higher priority)
150 2         23 $top_priority = $rr->priority;
151 2         9 $total_weight = 0;
152 2         5 @priority_records = ();
153             }
154              
155 19         215 $total_weight += $rr->weight;
156              
157 19 100       110 if ( $rr->weight > 0 ) {
158 11         71 push @priority_records, [ $total_weight, $rr ];
159             }
160             else {
161             # Zero-weigth elements must come first
162 8         54 unshift @priority_records, [ 0, $rr ];
163             }
164             }
165              
166 6 100       16 if ( 1 == scalar(@priority_records) ) {
167 2         5 my $record = shift @priority_records;
168 2         3 my ( $weighted_index, $rr ) = @$record;
169 2         7 return ( $rr->target, $rr->port );
170             }
171              
172             # Select first record according to RFC2782 weight ordering algorithm (page 3)
173 4         13 my $random_number = int(rand($total_weight + 1));
174              
175 4         7 foreach my $record (@priority_records) {
176 12         17 my ( $weighted_index, $rr ) = @$record;
177              
178 12 100       28 if ( $weighted_index >= $random_number ) {
179 4         11 return ( $rr->target, $rr->port );
180             }
181             }
182              
183 0         0 die 'There is something wrong with our SRV weight ordering algorithm';
184             }
185              
186             # Convert (target, port) to a full avatar base URL
187             sub build_url {
188 4     4 0 1710 my ( $target, $port, $https ) = @_;
189 4 100       13 return undef unless $target;
190              
191 3 50       10 my $url = $https ? 'https' : 'http' . '://' . $target;
192 3 100 66     35 if ( $port && !$https && ($port != 80) or $port && $https && ($port != 443) ) {
      100        
      66        
      33        
      66        
193 1         3 $url .= ':' . $port;
194             }
195 3         6 $url .= '/avatar';
196              
197 3         9 return $url;
198             }
199              
200             sub sanitize_target {
201 4     4 0 2172 my ( $target, $port ) = @_;
202              
203 4 100 66     27 unless ( defined $target && $target =~ m/^[0-9a-zA-Z\-.]+$/ ) {
204 1         4 return ( undef, undef );
205             }
206 3 100 66     18 unless ( defined $port && $port =~ m/^[0-9]{1,5}$/ ) {
207 1         3 return ( undef, undef );
208             }
209              
210 2         6 return ( $target, $port )
211             }
212              
213             sub federated_url {
214 16     16 0 52 my %args = @_;
215              
216 16         22 my $domain;
217 16 100       48 if ( exists $args{email} ) {
    50          
218 15         52 $domain = email_domain($args{email});
219             }
220             elsif ( exists $args{openid} ) {
221 0         0 $domain = openid_domain($args{openid});
222             }
223 16 100       33 return undef unless $domain;
224              
225 15         1306 require Net::DNS::Resolver;
226 15         97358 my $fast_resolver = Net::DNS::Resolver->new(retry => 1, tcp_timeout => 1, udp_timeout => 1, dnssec => 1);
227 15 100       7926 my $srv_prefix = $args{https} ? '_avatars-sec' : '_avatars';
228 15         100 my $packet = $fast_resolver->query($srv_prefix . '._tcp.' . $domain, 'SRV');
229              
230 15 50 33     91144 if ( $packet and $packet->answer ) {
231 0         0 my ( $target, $port ) = sanitize_target(srv_hostname($packet->answer));
232 0         0 return build_url($target, $port, $args{https});
233             }
234 15         181 return undef;
235             }
236              
237             sub lowercase_openid {
238 7     7 0 2771 my $openid = shift;
239              
240 7 100       51 if ( $openid =~ m@^([^:]+://[^/]+)(.*)@ ) {
241 5         17 $openid = (lc $1) . $2;
242             }
243 7         18 return $openid;
244             }
245              
246             sub libravatar_url {
247 19     19 1 36208 my %args = @_;
248 19         46 my $custom_base = defined $args{base};
249              
250 19 50 66     102 exists $args{email} or exists $args{openid} or exists $args{id} or
      66        
251             croak "Cannot generate a Libravatar URI without an email address, an OpenID or a gravatar id";
252              
253 19 50 33     270 if ( exists $args{email} and (exists $args{openid} or exists $args{id}) or
      66        
      0        
      33        
      33        
      33        
      66        
      33        
254             exists $args{openid} and (exists $args{email} or exists $args{id}) or
255             exists $args{id} and (exists $args{email} or exists $args{openid}) ) {
256 0         0 croak "Two or more identifiers (email, OpenID or gravatar id) were given. libravatar_url() only takes one";
257             }
258              
259 19         43 $defaults{base_http} = $Libravatar_Http_Base;
260 19         40 $defaults{base_https} = $Libravatar_Https_Base;
261 19         88 Gravatar::URL::_apply_defaults(\%args, \%defaults);
262              
263 19 100       125 if ( !$custom_base ) {
264 16         69 my $federated_url = federated_url(%args);
265 16 50       62 if ( $federated_url ) {
266 0         0 $args{base} = $federated_url;
267             }
268             }
269              
270 19 50       114 if ( exists $args{openid} ) {
271 0         0 $args{id} = sha256_hex(lowercase_openid($args{openid}));
272 0         0 undef $args{openid};
273             }
274 19         125 return gravatar_url(%args);
275             }
276              
277             =head1 LICENSE
278              
279             Copyright 2011, Francois Marier .
280              
281             This program is free software; you can redistribute it and/or
282             modify it under the same terms as Perl itself.
283              
284             See F
285              
286              
287             =head1 SEE ALSO
288              
289             L - The Libravatar web site
290              
291             L - The Libravatar API documentation
292              
293             =cut
294              
295             1;