root/branches/release-26/lib/MT/Util/Captcha.pm @ 1174

Revision 1174, 6.4 kB (checked in by bchoate, 23 months ago)

Updated copyright year for source.

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