root/branches/release-35/lib/MT/Util/Captcha.pm @ 1952

Revision 1952, 6.5 kB (checked in by bchoate, 20 months ago)

Append / after cgipath if it isn't present.

  • Property svn:keywords set to Id Author Date Revision
Line 
1# Movable Type (r) Open Source (C) 2001-2008 Six Apart, Ltd.
2# This program is distributed under the terms of the
3# GNU General Public License, version 2.
4#
5# $Id$
6
7package MT::Util::Captcha;
8
9use strict;
10use warnings;
11use base qw( MT::ErrorHandler );
12
13use constant READABLECHARS => '23456789abcdefghjkmnzpqrstuvwxyz';
14use constant WIDTH  => 25;
15use constant HEIGHT => 35;
16use constant LENGTH => 6;
17use constant EXPIRE => 60 * 10;
18
19use MT::Session;
20
21sub check_availability {
22    my $class = shift;
23
24    eval "require Image::Magick;";
25    if ($@) {
26        return MT->translate('Movable Type default CAPTCHA provider requires Image::Magick.');
27    }
28
29    my $cfg = MT->config;
30    my $base = $cfg->CaptchaSourceImageBase;
31    unless ($base) {
32        require File::Spec;
33        $base = File::Spec->catfile(MT->instance->config_dir, 'mt-static', 'images', 'captcha-source');
34        $base = undef unless (-d $base);
35    }
36    unless ($base) {
37        return MT->translate('You need to configure CaptchaSourceImageBase.');
38    }
39    undef;
40}
41
42sub form_fields {
43    my $self = shift;
44    my ($blog_id) = @_;
45
46    require MT::App;
47    my $token = MT::App->make_magic_token;
48    return q() unless $token;
49
50    my $cfg = MT->config;
51    my $cgipath = $cfg->CGIPath;
52    $cgipath .= '/' if $cgipath !~ m!/$!;
53    my $commentscript = $cfg->CommentScript;
54
55    my $caption = MT->translate('Captcha');
56    my $description = MT->translate('Type the characters you see in the picture above.');
57    return <<FORM_FIELDS;
58<div class="label"><label for="captcha_code">$caption:</label></div>
59<div class="field">
60<input type="hidden" name="token" value="$token" />
61<img src="$cgipath$commentscript/captcha/$blog_id/$token" width="150" height="35" /><br />
62<input name="captcha_code" id="captcha_code" value="" autocomplete="off" />
63<p>$description</p>
64</div>
65FORM_FIELDS
66}
67
68sub generate_captcha {
69    my $self = shift;
70    my ($app, $blog_id, $token) = @_;
71
72    my $code = $self->_generate_code(LENGTH());
73
74    my $sess = MT::Session->new;
75    $sess->id($code);
76    $sess->kind('CA'); #CA == CaptchA
77    $sess->start(time);
78    $sess->name($token);
79    $sess->save or
80        $app->error($sess->errstr), return undef;
81   
82    my $image_data = $self->_generate_captcha($app, $code, 'png') or
83        return undef; 
84
85    return $image_data; 
86}
87
88sub validate_captcha {
89    my $self = shift;
90    my ($app) = @_;
91
92    my $token = $app->param('token');
93    my $code = $app->param('captcha_code');
94
95    my $from = time - EXPIRE();
96    MT::Session->remove({ kind => 'CA', start => [undef, $from] }, { range => { start => 1 }});
97
98    my $sess = MT::Session->load({ id => $code, name => $token, kind => 'CA' });
99    return 0 unless $sess;
100    if ($sess->start() < (time - EXPIRE())) {
101        $sess->remove;
102        return 0;
103    }
104    $sess->remove;
105    return 1;
106}
107
108sub _makerandom {
109    my $size = shift;
110
111    my $bytes = int($size / 8) + ($size % 8 ? 1 : 0);
112
113    my $rand;
114    if (-e "/dev/urandom") {
115        my $fh;
116        open($fh, '/dev/urandom')
117            or die "Couldn't open /dev/urandom";
118        my $got = sysread $fh, $rand, $bytes;
119        die "Didn't read all bytes from urandom" unless $got == $bytes;
120        close $fh;
121    } else {
122        for (1..$bytes) {
123            $rand .= chr(int(rand(256)));
124        }
125    }
126    $rand;
127}
128
129sub _generate_code {
130    my $self = shift;
131    my($len) = @_;
132
133    my $code = '';
134
135    my $genval = unpack('H*', _makerandom($len*2*8/2));
136
137    # Cycle through the octets pulling off the lower 5 bits then mapped into
138    # our acceptable characters
139    foreach my $i (0..($len-1)) {
140      my $byte = ord(pack('H2', substr($genval, $i*2, 2)));
141      my $x = ($byte & 31);
142
143      $code .= substr(READABLECHARS(), $byte & 31, 1);
144    }
145
146    return $code;
147}
148
149sub _generate_captcha {
150    my $self = shift;
151    my ($app, $code, $format) = @_;
152    $format ||= 'png';
153    my $len = LENGTH();
154
155    my $cfg = $app->config;
156    my $base = $cfg->CaptchaSourceImageBase;
157    unless ($base) {
158        require File::Spec;
159        $base = File::Spec->catfile(MT->instance->config_dir, 'mt-static', 'images', 'captcha-source');
160        $base = undef unless (-d $base);
161    }
162    return $app->error($app->translate('You need to configure CaptchaSourceImageBase.'))
163        unless $base;
164
165    require Image::Magick;
166    my $imbase = Image::Magick->new(magick=>'png')
167        or return $app->error($app->translate("Image creation failed."));
168
169    # Read the predefined letter PNG for each letter in $code
170    my $x = $imbase->Read(map { File::Spec->catfile($base, $_ . '.png') }
171                          split(//, $code));
172    if ($x) {
173        return $app->error($app->translate("Image error: [_1]", $x));
174    }
175
176    # Futz with the size and blurriness of each letter
177    foreach my $i (0..($len - 1)) {
178        my $a = int rand int(WIDTH() / 14);
179        my $b = int rand int(HEIGHT() / 12);
180
181        $imbase->[$i]->Resize(width => $a, height => $b, blur => rand(3));
182    }
183
184    # Combine all the individual tiles into one block
185    my $tile_geom    = join('x', $len, 1);
186    my $geometry_str = join('x', WIDTH(), HEIGHT());
187    my $im = $imbase->Montage(geometry => $geometry_str,
188                              tile     => $tile_geom);
189    $im->Blur();
190
191    # Add some lines and dots to the image
192    for my $i (0..($len * WIDTH() * HEIGHT() / 14+200-1)) {
193        my $a = int rand($len * WIDTH());
194        my $b = int rand HEIGHT();
195        my $c = int rand($len * WIDTH());
196        my $d = int rand HEIGHT();
197        my $index = $im->Get("pixel[$a, $b]");
198
199
200        if ($i < ($len * WIDTH() * HEIGHT() / 14+200) / 100) {
201            $im->Draw(primitive => 'line',
202                      stroke    => $index,
203                      points    => "$a, $b, $c, $d");
204        } elsif ($i < ($len * WIDTH() * HEIGHT() / 14+200) / 2) {
205            $im->Set("pixel[$c, $d]" => $index);
206        } else {
207            $im->Set("pixel[$c, $d]" => "black");
208        }
209    }
210
211    # Read in the background file
212    my $a = int rand(5) + 1;
213    my $background = Image::Magick->new();
214    $background->Read(File::Spec->catfile($base, 'background' . $a . '.png'));
215    $background->Resize(width => ($len * WIDTH()), height => HEIGHT());
216    $im->Composite(compose => "Bumpmap",
217                   tile    => 'False',
218                   image   => $background);
219    $im->Modulate(brightness => 105);
220    $im->Border(fill     => 'black',
221                width    => 1,
222                height   => 1,
223                geometry => join('x', WIDTH() * $len, HEIGHT()));
224
225    my @blobs = $im->ImageToBlob(magick=>$format);
226    return $blobs[0];
227}
228
2291;
Note: See TracBrowser for help on using the browser.