1 # This module is part of debbugs, and is released
2 # under the terms of the GPL version 2, or any later version. See the
3 # file README and COPYING for more information.
4 # Copyright 2013 by Don Armstrong <don@donarmstrong.com>.
6 package Debbugs::Libravatar;
10 Debbugs::Libravatar -- Libravatar service handler (mod_perl)
14 <Location /libravatar>
15 SetHandler perl-script
16 PerlResponseHandler Debbugs::Libravatar
21 Debbugs::Libravatar is a libravatar service handler which will serve
22 libravatar requests. It also contains utility routines which are used
23 by the libravatar.cgi script for those who do not have mod_perl.
33 use vars qw($VERSION $DEBUG %EXPORT_TAGS @EXPORT_OK @EXPORT);
34 use base qw(Exporter);
36 use Debbugs::Config qw(:config);
37 use Debbugs::Common qw(:lock);
40 use Debbugs::CGI qw(cgi_parameters);
41 use Digest::MD5 qw(md5_hex);
43 use File::Temp qw(tempfile);
46 ($VERSION) = q$Revision$ =~ /^Revision:\s+([^\s+])/;
47 $DEBUG = 0 unless defined $DEBUG;
50 %EXPORT_TAGS = (libravatar => [qw(cache_valid serve_cache retrieve_libravatar)]
53 Exporter::export_ok_tags(keys %EXPORT_TAGS);
54 $EXPORT_TAGS{all} = [@EXPORT_OK];
58 my ($cache_location) = @_;
59 if (-e $cache_location) {
60 if (time - (stat($cache_location))[9] < 60*60) {
67 =item retreive_libravatar
69 $cache_location = retreive_libravatar(location => $cache_location,
70 email => lc($param{email}),
73 Returns the cache location where a specific avatar can be loaded. If
74 there isn't a matching avatar, or there is an error, returns undef.
79 sub retreive_libravatar{
90 my $cache_location = $param{location};
91 $cache_location =~ s/\.[^\.]+$//;
92 # take out a lock on the cache location so that if another request
93 # is made while we are serving this one, we don't do double work
94 my ($fh,$lockfile,$errors) =
95 simple_filelock($cache_location.'.lock',20,0.5);
99 # figure out if the cache is now valid; if it is, return the
101 my $temp_location = cache_location(email => $param{email});
102 if (cache_valid($temp_location)) {
103 return $temp_location;
108 my $uri = libravatar_url(email => $param{email},
111 my $ua = LWP::UserAgent->new(agent => 'Debbugs libravatar service (not Mozilla)',
113 $ua->from($config{maintainer});
114 # if we don't get an avatar within 10 seconds, return so we
115 # don't block forever
117 # if the avatar is bigger than 30K, we don't want it either
118 $ua->max_size(30*1024);
119 my $r = $ua->get($uri);
120 if (not $r->is_success()) {
121 die "Not successful in request";
123 my $aborted = $r->header('Client-Aborted');
124 # if we exceeded max size, I'm not sure if we'll be
125 # successfull or not, but regardless, there will be a
126 # Client-Aborted header. Stop here if that header is defined.
127 die "Client aborted header" if defined $aborted;
128 my $type = $r->header('Content-Type');
129 # if there's no content type, or it's not one we like, we won't
130 # bother going further
131 die "No content type" if not defined $type;
132 die "Wrong content type" if not $type =~ m{^image/([^/]+)$};
133 $dest_type = $type_mapping{$1};
134 die "No dest type" if not defined $dest_type;
135 # undo any content encoding
136 $r->decode() or die "Unable to decode content encoding";
137 # ok, now we need to convert it from whatever it is into a
138 # format that we actually like
139 my ($temp_fh,$temp_fn) = tempfile() or
140 die "Unable to create temporary file";
142 print {$temp_fh} $r->content() or
143 die "Unable to print to temp file";
145 ### resize all images to 80x80 and strip comments out of
146 ### them. If convert has a bug, it would be possible for
147 ### this to be an attack vector, but hopefully minimizing
148 ### the size above, and requiring proper mime types will
149 ### minimize that slightly. Doing this will at least make
150 ### it harder for malicious web images to harm our users
151 system('convert','-resize','80x80',
154 $cache_location.'.'.$dest_type) == 0 or
155 die "convert file failed";
159 unlink($cache_location.'.'.$dest_type) if -e $cache_location.'.'.$dest_type;
160 unlink($temp_fn) if -e $temp_fn;
161 die "Unable to convert image";
165 # there was some kind of error; return undef and unlock the
167 simple_unlockfile($fh,$lockfile);
170 simple_unlockfile($fh,$lockfile);
171 return $cache_location.'.'.$dest_type;
177 if (exists $param{md5sum}) {
178 $md5sum = $param{md5sum};
179 }elsif (exists $param{email}) {
180 $md5sum = md5_hex(lc($param{email}));
182 croak("cache_location must be called with one of md5sum or email");
184 for my $ext (qw(.png .jpg)) {
185 if (-e $config{libravatar_cache_dir}.'/'.$md5sum.$ext) {
186 return $config{libravatar_cache_dir}.'/'.$md5sum.$ext;
189 return $config{libravatar_cache_dir}.'/'.$md5sum;
192 ## the following is mod_perl specific
195 if (exists $ENV{MOD_PERL_API_VERSION}) {
196 if ($ENV{MOD_PERL_API_VERSION} == 2) {
197 require Apache2::RequestIO;
198 require Apache2::RequestRec;
199 require Apache2::RequestUtil;
200 require Apache2::Const;
203 APR::Const->import(-compile => qw(FINFO_NORM));
204 Apache2::Const->import(-compile => qw(OK DECLINED FORBIDDEN NOT_FOUND HTTP_NOT_MODIFIED));
206 die "Unsupported mod perl api; mod_perl 2.0.0 or later is required";
212 die "Calling handler only makes sense if this is running under mod_perl" unless exists $ENV{MOD_PERL_API_VERSION};
213 my $r = shift or Apache2::RequestUtil->request;
215 # we only want GET or HEAD requests
216 unless ($r->method eq 'HEAD' or $r->method eq 'GET') {
217 return Apache2::Const::DECLINED();
219 $r->headers_out->{"X-Powered-By"} = "Debbugs libravatar";
222 # subtract out location
223 my $location = $r->location();
224 my ($email) = $uri =~ m/\Q$location\E\/?(.*)$/;
225 if (not length $email) {
226 return Apache2::Const::NOT_FOUND;
228 my $q = CGI::Simple->new();
229 my %param = cgi_parameters(query => $q,
230 single => [qw(avatar)],
231 default => {avatar => 'yes',
234 if ($param{avatar} ne 'yes' or not defined $email or not length $email) {
235 serve_cache_mod_perl('',$r);
236 return Apache2::Const::DECLINED();
238 # figure out what the md5sum of the e-mail is.
239 my $cache_location = cache_location(email => $email);
240 # if we've got it, and it's less than one hour old, return it.
241 if (cache_valid($cache_location)) {
242 serve_cache_mod_perl($cache_location,$r);
243 return Apache2::Const::DECLINED();
245 $cache_location = retreive_libravatar(location => $cache_location,
248 if (not defined $cache_location) {
249 # failure, serve the default image
250 serve_cache_mod_perl('',$r);
251 return Apache2::Const::DECLINED();
253 serve_cache_mod_perl($cache_location,$r);
254 return Apache2::Const::DECLINED();
259 sub serve_cache_mod_perl {
260 my ($cache_location,$r) = @_;
261 if (not defined $cache_location or not length $cache_location) {
262 # serve the default image
263 $cache_location = $config{libravatar_default_image};
265 $r->filename($cache_location);
267 $r->finfo(APR::Finfo::stat($cache_location, APR::Const::FINFO_NORM, $r->pool));