File Coverage

blib/lib/Authen/Captcha.pm
Criterion Covered Total %
statement 6 8 75.0
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 9 11 81.8


line stmt bran cond sub pod time code
1             package Authen::Captcha;
2              
3             # $Source: /usr/local/cvs/Captcha/pm/Captcha.pm,v $
4             # $Revision: 1.23 $
5             # $Date: 2003/12/18 04:44:34 $
6             # $Author: jmiller $
7             # License: GNU General Public License Version 2 (see license.txt)
8              
9 1     1   5829 use 5.00503;
  1         2  
10 1     1   4 use strict;
  1         0  
  1         17  
11 1     1   1054 use GD;
  0            
  0            
12             use String::Random qw(random_regex);
13             use Carp;
14             # these are used to find default images dir
15             use File::Basename;
16             use File::Spec;
17              
18             use vars qw($VERSION);
19              
20             $VERSION = sprintf "%d.%03d_001", q$Revision: 1.23 $ =~ /(\d+)/g;
21              
22             # get our file name, used to find the default images
23             my $default_images_folder;
24             {
25             my $this_file = __FILE__;
26             my $this_dir = dirname($this_file);
27             my @this_dirs = File::Spec->splitdir( $this_dir );
28             $default_images_folder = File::Spec->catdir(@this_dirs,'Captcha','images');
29             }
30              
31             my $num_of_soundfile_versions = 10;
32              
33             # Preloaded methods go here.
34              
35             sub new
36             {
37             my ($this) = shift;
38             my $class = ref($this) || $this;
39             my $self = {};
40             bless( $self, $class );
41            
42             my %opts = @_;
43              
44             # default character source images
45             my $type = defined($opts{type}) ? $opts{type} : 'image';
46             $self->type($type);
47             my $src_images = (defined($opts{images_folder}) && (-d $opts{images_folder}))
48             ? $opts{images_folder} : $default_images_folder;
49             $self->images_folder($src_images);
50              
51             my $debug = (defined($opts{debug}) && ($opts{debug} =~ /^\d+$/))
52             ? $opts{debug} : 0;
53             $self->debug($debug);
54             $self->data_folder($opts{data_folder}) if($opts{data_folder});
55             $self->output_folder($opts{output_folder}) if($opts{output_folder});
56             my $expire = (defined($opts{expire}) && ($opts{expire} =~ /^\d+$/))
57             ? $opts{expire} : 300;
58             $self->expire($expire);
59             my $width = (defined($opts{width}) && ($opts{width} =~ /^\d+$/))
60             ? $opts{width} : 25;
61             $self->width($width);
62             my $height = (defined($opts{height}) && ($opts{height} =~ /^\d+$/))
63             ? $opts{height} : 35;
64             $self->height($height);
65             my $keep_failures = (defined($opts{keep_failures}) && $opts{keep_failures})
66             ? 1 : 0;
67             $self->keep_failures($keep_failures);
68            
69             # create a random seed if perl version less than 5.004
70             if ($] < 5.005)
71             { # have to seed rand. using a fairly good seed
72             srand( time() ^ ($$ + ($$ << 15)) );
73             } # else, we're just going to let perl do it's thing
74              
75             return $self;
76             }
77              
78             sub type
79             {
80             ref(my $self = shift) or croak "instance variable needed";
81             if (@_)
82             {
83             if ($_[0] =~ /^(jpg|png|gif|image|picture)$/i)
84             {
85             $self->{_type} = 'image';
86             } elsif ($_[0] =~ /^(sound|snd|wav|mp3)$/i) {
87             $self->{_type} = 'sound';
88             }
89             return $self->{_type};
90             } else {
91             return $self->{_type};
92             }
93             }
94              
95             sub debug
96             {
97             ref(my $self = shift) or croak "instance variable needed";
98             if (@_)
99             {
100             $self->{_debug} = $_[0];
101             return $self->{_debug};
102             } else {
103             return $self->{_debug};
104             }
105             }
106              
107             sub keep_failures
108             {
109             ref(my $self = shift) or croak "instance variable needed";
110             if (@_)
111             {
112             croak "keep_failures must be a zero or one" unless ($_[0] =~ /^[01]$/);
113             $self->{_keep_failures} = $_[0];
114             return $self->{_keep_failures};
115             } else {
116             return $self->{_keep_failures};
117             }
118             }
119              
120             sub expire
121             {
122             ref(my $self = shift) or croak "instance variable needed";
123             if (@_)
124             {
125             croak "expire must be a possitive integer" unless ($_[0] =~ /^\d+$/);
126             $self->{_expire} = $_[0];
127             return $self->{_expire};
128             } else {
129             return $self->{_expire};
130             }
131             }
132              
133             sub width
134             {
135             ref(my $self = shift) or croak "instance variable needed";
136             if (@_)
137             {
138             croak "width must be a possitive integer" unless ($_[0] =~ /^\d+$/);
139             $self->{_width} = $_[0];
140             return $self->{_width};
141             } else {
142             return $self->{_width};
143             }
144             }
145              
146             sub height
147             {
148             ref(my $self = shift) or croak "instance variable needed";
149             if (@_)
150             {
151             croak "height must be a possitive integer" unless ($_[0] =~ /^\d+$/);
152             $self->{_height} = $_[0];
153             return $self->{_height};
154             } else {
155             return $self->{_height};
156             }
157             }
158              
159             sub output_folder
160             {
161            
162             ref(my $self = shift) or croak "instance variable needed";
163             if (@_)
164             { # it's a setter
165             $self->{_output_folder} = $_[0];
166             return $self->{_output_folder};
167             } else {
168             return $self->{_output_folder};
169             }
170             }
171              
172             sub images_folder
173             {
174             ref(my $self = shift) or croak "instance variable needed";
175             if (@_)
176             { # it's a setter
177             $self->{_images_folder} = $_[0];
178             return $self->{_images_folder};
179             } else {
180             return $self->{_images_folder};
181             }
182             }
183              
184             sub data_folder
185             {
186             ref(my $self = shift) or croak "instance variable needed";
187             if (@_)
188             { # it's a setter
189             $self->{_data_folder} = $_[0];
190             return $self->{_data_folder};
191             } else {
192             return $self->{_data_folder};
193             }
194             }
195              
196              
197             sub check_code
198             {
199             ref(my $self = shift) or croak "instance variable needed";
200             my ($code, $token) = @_;
201              
202             $code = lc($code);
203            
204             warn "$code $token\n" if($self->debug() >= 2);
205              
206             my $current_time = time;
207             # solution was not found in database (well, yet :)
208             my $return_value = -2;
209             my $database_file = File::Spec->catfile($self->data_folder(),"codes.txt");
210              
211             # create database file if it doesn't already exist
212             $self->_touch_file($database_file);
213              
214             # zeros (0) and ones (1) are not part of the code
215             # they could be confused with (o) and (l), so we swap them in
216             $code =~ tr/01/ol/;
217              
218             # pull in current database
219             warn "Open File: $database_file\n" if($self->debug() >= 2);
220             $self->_get_exclusive_lock();
221             open (DATA, "<$database_file") or die "Can't open File: $database_file\n";
222             my @data=;
223             close(DATA);
224             warn "Close File: $database_file\n" if($self->debug() >= 2);
225              
226             my $passed=0;
227             # $new_data will hold the part of the database we want to keep and
228             # write back out
229             my $new_data = "";
230             foreach my $line (@data)
231             {
232             $line =~ s/\n//;
233             my ($data_time,$data_token,$data_code) = $line =~ m/(^\d+)::([a-f0-9]{32})::(.*)$/
234             or next;
235              
236             my $png_file = File::Spec->catfile($self->output_folder(),$data_token . ".png");
237             if ($data_token eq $token)
238             {
239              
240             # the token was found in the database
241             if (($current_time - $data_time) > $self->expire())
242             {
243             warn "Crypt Found But Expired\n" if($self->debug() >= 2);
244             # the token was found but has expired
245             $return_value = -1;
246             } else {
247             warn "Match Crypt in File Crypt: $token\n" if($self->debug() >= 2);
248             }
249              
250             if ( ($data_code eq $code) && ($return_value != -1) )
251             {
252             warn "Match: " . $data_token . " And " . $token . "\n" if($self->debug() >= 2);
253             # solution was correct and was found in database - passed
254             $return_value = 1;
255             }
256              
257             if ( $return_value < ($self->keep_failures() ? -1 : -2) )
258             {
259             warn "No Match: " . $data_token . " And " . $token . "\n" if($self->debug() >= 2);
260             # solution was wrong, not expired, and we're keeping failures
261             $new_data .= $line."\n";
262             } else {
263             # remove the found token so it can't be used again
264             warn "Unlink File: " . $png_file . "\n" if($self->debug() >= 2);
265             unlink($png_file) or carp("Can't remove png file [$png_file]\n");
266             }
267              
268             if ( $return_value == -2 ) {
269             # incorrect solution
270             $return_value = -3;
271             }
272              
273             } elsif (($current_time - $data_time) > $self->expire()) {
274             # removed expired token
275             warn "Removing Expired Crypt File: " . $png_file ."\n" if($self->debug() >= 2);
276             unlink($png_file) or carp("Can't remove png file [$png_file]\n");
277             } else {
278             # token not found or expired, keep it
279             $new_data .= $line."\n";
280             }
281             }
282              
283             # update database
284             open(DATA,">$database_file") or die "Can't open File: $database_file\n";
285             print DATA $new_data;
286             close(DATA);
287             $self->_release_lock();
288            
289             return $return_value;
290             }
291              
292              
293             sub _open_lock_file {
294             my $self = shift;
295             my $file_name = shift;
296             open(LOCK, ">>$file_name") or die "Error opening lockfile $file_name: $!\n";
297             }
298              
299             sub _get_shared_lock {
300             my $self = shift;
301             my $lock_file_name = File::Spec->catfile($self->data_folder(),"codes.lock");
302             $self->_open_lock_file($lock_file_name);
303              
304             # shared lock
305             flock(LOCK, 1) or die "Error locking lockfile in shared mode: $!\n";
306             }
307              
308             sub _get_exclusive_lock {
309             my $self = shift;
310             my $lock_file_name = File::Spec->catfile($self->data_folder(),"codes.lock");
311             $self->_open_lock_file($lock_file_name);
312              
313             # exclusive lock
314             flock(LOCK, 2) or die "Error locking lockfile exclusively: $!\n";
315             }
316              
317             sub _release_lock {
318             my $self = shift;
319             flock(LOCK, 8) or die "Error unlocking lockfile: $!\n";
320             close(LOCK);
321             }
322              
323             sub _touch_file
324             {
325             ref(my $self = shift) or croak "instance variable needed";
326             my $file = shift;
327             # create database file if it doesn't already exist
328             if (! -e $file)
329             {
330             open (DATA, ">>$file") or die "Can't create File: $file\n";
331             close(DATA);
332             }
333             }
334              
335             sub generate_random_string
336             {
337             ref(my $self = shift) or croak "instance variable needed";
338             my $length = shift;
339              
340             # generate a new code
341             my $code = "";
342             for(my $i=0; $i < $length; $i++)
343             {
344             my $char;
345             my $list = int(rand 4) +1;
346             if ($list == 1)
347             { # choose a number 1/4 of the time
348             $char = int(rand 7)+50;
349             } else { # choose a letter 3/4 of the time
350             $char = int(rand 25)+97;
351             }
352             $char = chr($char);
353             $code .= $char;
354             }
355             return $code;
356             }
357              
358             sub _save_code
359             {
360             ref(my $self = shift) or croak "instance variable needed";
361             my $code = shift;
362             my $token = shift;
363              
364             my $database_file = File::Spec->catfile($self->data_folder(),'codes.txt');
365              
366             # set a variable with the current time
367             my $current_time = time;
368              
369             # create database file if it doesn't already exist
370             $self->_touch_file($database_file);
371              
372             # clean expired codes and images
373             $self->_get_exclusive_lock();
374              
375             open (DATA, "<$database_file") or die "Can't open File: $database_file\n";
376             my @data=;
377             close(DATA);
378            
379             my $new_data = "";
380             foreach my $line (@data)
381             {
382             $line =~ s/\n//;
383             my ($data_time,$data_token,$data_code) = $line =~ m/(^\d+)::([a-f0-9]{32})::(.*)$/
384             or next;
385             if ( (($current_time - $data_time) > ($self->expire())) ||
386             ($data_token eq $token) )
387             { # remove expired captcha, or a dup
388             my $png_file = File::Spec->catfile($self->output_folder(),$data_token . ".png");
389             unlink($png_file) or carp("Can't remove png file [$png_file]\n");
390             } else {
391             $new_data .= $line."\n";
392             }
393             }
394            
395             # save the code to database
396             warn "open File: $database_file\n" if($self->debug() >= 2);
397             open(DATA,">$database_file") or die "Can't open File: $database_file\n";
398             warn "-->>" . $new_data . "\n" if($self->debug() >= 2);
399             warn "-->>" . $current_time . "::" . $token."::".$code."\n" if($self->debug() >= 2);
400             print DATA $new_data;
401             print DATA $current_time."::".$token."::".$code."\n";
402             close(DATA);
403             $self->_release_lock();
404             warn "Close File: $database_file\n" if($self->debug() >= 2);
405             }
406              
407             sub create_image_file
408             {
409             ref(my $self = shift) or croak "instance variable needed";
410             my $code = shift;
411              
412             my $length = length($code);
413             my $im_width = $self->width();
414             # create a new image and color
415             my $im = new GD::Image(($im_width * $length),$self->height());
416             my $black = $im->colorAllocate(0,0,0);
417              
418             # copy the character images into the code graphic
419             for(my $i=0; $i < $length; $i++)
420             {
421             my $letter = substr($code,$i,1);
422             my $letter_png = File::Spec->catfile($self->images_folder(),$letter . ".png");
423             my $source = new GD::Image($letter_png);
424             $im->copy($source,($i*($self->width()),0,0,0,$self->width(),$self->height()));
425             my $a = int(rand (int(($self->width())/14)))+0;
426             my $b = int(rand (int(($self->height())/12)))+0;
427             my $c = int(rand (int(($self->width())/3)))-(int(($self->width())/5));
428             my $d = int(rand (int(($self->height())/3)))-(int(($self->height())/5));
429             $im->copyResized($source,($i*($self->width()))+$a,$b,0,0,($self->width())+$c,($self->height())+$d,$self->width(),$self->height());
430             }
431            
432             # distort the code graphic
433             for(my $i=0; $i<($length*($self->width())*($self->height())/14+200); $i++)
434             {
435             my $a = (int(rand ($length*($self->width())))+0);
436             my $b = (int(rand $self->height())+0);
437             my $c = (int(rand ($length*($self->width())))+0);
438             my $d = (int(rand $self->height())+0);
439             my $index = $im->getPixel($a,$b);
440             if ($i < (($length*($self->width())*($self->height())/14+200)/100))
441             {
442             $im->line($a,$b,$c,$d,$index);
443             } elsif ($i < (($length*($self->width())*($self->height())/14+200)/2)) {
444             $im->setPixel($c,$d,$index);
445             } else {
446             $im->setPixel($c,$d,$black);
447             }
448             }
449            
450             # generate a background
451             my $a = int(rand 5)+1;
452             my $background_img = File::Spec->catfile($self->images_folder(),"background" . $a . ".png");
453             my $source = new GD::Image($background_img);
454             my ($background_width,$background_height) = $source->getBounds();
455             my $b = int(rand (int($background_width/13)))+0;
456             my $c = int(rand (int($background_height/7)))+0;
457             my $d = int(rand (int($background_width/13)))+0;
458             my $e = int(rand (int($background_height/7)))+0;
459             my $source2 = new GD::Image(($length*($self->width())),$self->height());
460             $source2->copyResized($source,0,0,$b,$c,($length*($self->width())),$self->height(),$background_width-$b-$d,$background_height-$c-$e);
461            
462             # merge the background onto the image
463             $im->copyMerge($source2,0,0,0,0,($length*($self->width())),$self->height(),40);
464            
465             # add a border
466             $im->rectangle(0,0,((($length)*($self->width()))-1),(($self->height())-1),$black);
467              
468             # save the image to file
469             my $png_data = $im->png;
470              
471             return \$png_data;
472             }
473              
474             sub create_sound_file
475             {
476             ref(my $self = shift) or croak "instance variable needed";
477             my $code = shift;
478             my $length = length($code);
479              
480             my @chars = split('',$code);
481             my $snd_file;
482             local $/; # input record separator. So we can slurp the data.
483             # get a random voice speaking the code
484             foreach my $char (@chars)
485             {
486             my $voice = int(rand $num_of_soundfile_versions) + 1;
487             my $src_name = File::Spec->catfile($self->images_folder(),$voice, $char . ".wav");
488             warn "Open File: $src_name\n" if($self->debug() >= 2);
489             open (FILE,"< $src_name") or die "Can't open File: $src_name\n";
490             flock FILE, 1; # read lock
491             binmode FILE;
492             $snd_file .= ;
493             close FILE;
494             warn "Close File: $src_name\n" if($self->debug() >= 2);
495             }
496             return \$snd_file;
497             }
498              
499             sub _save_file
500             {
501             ref(my $self = shift) or croak "instance variable needed";
502             my $file_ref = shift;
503             my $file_name = shift;
504              
505             warn "Open File: $file_name\n" if($self->debug() >= 2);
506             open (FILE,">$file_name") or die "Can't open File: $file_name \n";
507             flock FILE, 2; # write lock
508             binmode FILE;
509             print FILE $$file_ref;
510             close FILE;
511             warn "Close File: $file_name\n" if($self->debug() >= 2);
512             }
513              
514             sub generate_code
515             {
516             ref(my $self = shift) or croak "instance variable needed";
517             my $length = shift;
518              
519             my $code = $self->generate_random_string($length);
520             my $token = random_regex('[a-f0-9]{32}');
521              
522             my ($captcha_data_ref,$output_filename);
523             if ($self->type() eq 'image')
524             {
525             $captcha_data_ref = $self->create_image_file($code);
526             $output_filename = File::Spec->catfile($self->output_folder(),$token . ".png");
527             } elsif ($self->type() eq 'sound') {
528             $captcha_data_ref = $self->create_sound_file($code);
529             $output_filename = File::Spec->catfile($self->output_folder(),$token . ".wav");
530             } else {
531             croak "invalid captcha type [" . $self->type() . "]";
532             }
533              
534             $self->_save_file($captcha_data_ref,$output_filename);
535             $self->_save_code($code,$token);
536              
537             # return token (token)... or, if they want it, the code as well.
538             return wantarray ? ($token,$code) : $token;
539             }
540              
541             sub version
542             {
543             return $VERSION;
544             }
545              
546             1;
547             __END__