#!/usr/bin/perl use strict; use vars qw(%maint %maintinfo); use LJ::Captcha qw{}; use LJ::Blob qw{}; use File::Temp qw{tempdir}; use File::Path qw{rmtree}; use File::Spec qw{}; our ( $FakeUserId, $ClusterId, $Digits, $DigitCount, $ExpireThresUser, $ExpireThresNoUser ); # Data for code-generation $Digits = "abcdefghkmnpqrstuvwzyz23456789"; $DigitCount = length( $Digits ); # Maximum age of answered captchas. this is just # for double-click protection. $ExpireThresUser = 2 * 60; # two minutes # 24 hours for captchas which were given out but not answered. # (they might leave their browser window open or something) $ExpireThresNoUser = 24 * 3600; # 1 day ##################################################################### ### F U N C T I O N S ##################################################################### ### Read a file in as a scalar and return it sub readfile ($) { my ( $filename ) = @_; open my $fh, "<$filename" or die "open: $filename: $!"; local $/ = undef; my $data = <$fh>; return $data; } ### Generate an n-character challenge code sub gencode ($) { my ( $digits ) = @_; my $code = ''; for ( 1..$digits ) { $code .= substr( $Digits, int(rand($DigitCount)), 1 ); } return $code; } ##################################################################### ### M A I N T E N A N C E T A S K S ##################################################################### $maintinfo{gen_audio_captchas}{opts}{locking} = "per_host"; $maint{gen_audio_captchas} = sub { my ( $u, # Fake user record for Blob::put $dbh, # Database handle (writer) $count, # Count of currently-extant audio challenges $need, # How many we need to still create $make, # how many we're actually going to create this round $tmpdir, # Temporary working directory $code, # The generated challenge code $wav, # Wav file $data, # Wav file data $err, # Error-message ref for Blob::put calls $capid, # Captcha row id $anum, # Deseries-ifier value ); print "-I- Generating new audio captchas...\n"; $dbh = LJ::get_dbh({raw=>1}, "master") or die "Failed to get_db_writer()"; $dbh->do("SET wait_timeout=28800"); # Count how many challenges there are currently $count = $dbh->selectrow_array(q{ SELECT COUNT(*) FROM captchas WHERE type = 'audio' AND issuetime = 0 }); my $MaxItems = $LJ::CAPTCHA_AUDIO_PREGEN || 500; # If there are enough, don't generate any more print "Current count is $count of $MaxItems..."; if ( $count >= $MaxItems ) { print "already have enough.\n"; return; } else { $make = $need = $MaxItems - $count; $make = $LJ::CAPTCHA_AUDIO_MAKE if defined $LJ::CAPTCHA_AUDIO_MAKE && $make > $LJ::CAPTCHA_AUDIO_MAKE; print "generating $make new audio challenges.\n"; } # Clean up any old audio directories lying about from failed generations # before. In theory, File::Temp::tempdir() is supposed to clean them up # itself, but it doesn't appear to be doing so. foreach my $olddir ( glob "audio_captchas_*" ) { # If it's been more than an hour since it's been changed from the # starting time of the script, kill it if ( (-M $olddir) * 24 > 1 ) { print "cleaning up old working temp directory ($olddir).\n"; rmtree( $olddir ) or die "rmtree: $olddir: $!"; } } # Load the system user for Blob::put() and create an auto-cleaning temp # directory for audio generation $u = LJ::load_user( "system" ) or die "Couldn't load the system user."; $tmpdir = tempdir( "audio_captchas_XXXXXX", CLEANUP => 0 ); # target location my $location = $LJ::CAPTCHA_MOGILEFS ? 'mogile' : 'blob'; # Generate the challenges for ( my $i = 0; $i < $make; $i++ ) { print "Generating audio $i..."; ( $wav, $code ) = LJ::Captcha::generate_audio( $tmpdir ); $data = readfile( $wav ); unlink $wav or die "unlink: $wav: $!"; # Generate the capid + anum print "generating new capid/anum..."; $capid = LJ::alloc_global_counter( 'C' ); die "Couldn't allocate capid" unless $capid; $anum = int( rand 65_535 ); # Insert the blob print "uploading (capid = $capid, anum = $anum)..."; if ($location eq 'mogile') { my $mogfs = LJ::mogclient(); # force load die "Requested to store captchas on MogileFS, but it's not loaded.\n" unless $mogfs; my $fh = $mogfs->new_file("captcha:$capid", 'captcha') or die "Unable to contact MogileFS server for storage.\n"; $fh->print($data); $fh->close or die "Unable to save captcha to MogileFS server: $@\n"; } else { LJ::Blob::put( $u, 'captcha_audio', 'wav', $capid, $data, \$err ) or die "Error uploading to media server: $err"; } # Insert the captcha into the DB. If it fails for some reason, delete # the just-uploaded file from the media storage system too. print "inserting (code = $code)..."; my $rval = eval { $dbh->do(q{ INSERT INTO captchas( capid, type, location, answer, anum ) VALUES ( ?, 'audio', ?, ?, ? ) }, undef, $capid, $location, $code, $anum); }; if ( !$rval || $@ ) { my $err = $@ || $dbh->errstr; if ( $location eq 'mogile' ) { LJ::mogclient()->delete( "captcha:$capid" ); } else { LJ::Blob::delete( $u, 'captcha_audio', 'wav', $capid ); } die "audio captcha insert error on ($capid, $location, $code, $anum): $err"; } print "done.\n"; } print "cleaning up working temporary directory ($tmpdir).\n"; rmtree( $tmpdir ) or die "Failed directory cleanup: $!"; print "done. Created $make new audio captchas.\n"; return 1; }; $maintinfo{gen_image_captchas}{opts}{locking} = "per_host"; $maint{gen_image_captchas} = sub { my ( $u, # Fake user record for Blob::put $dbh, # Database handle (writer) $count, # Count of currently-extant audio challenges $need, # How many we need to still create $code, # The generated challenge code $png, # PNG data $err, # Error-message ref for Blob::put calls $capid, # Captcha row id $anum, # Deseries-ifier value ); print "-I- Generating new image captchas...\n"; $dbh = LJ::get_dbh({raw=>1}, "master") or die "Failed to get_db_writer()"; $dbh->do("SET wait_timeout=28800"); # Count how many challenges there are currently $count = $dbh->selectrow_array(q{ SELECT COUNT(*) FROM captchas WHERE type = 'image' AND issuetime = 0 }); my $MaxItems = $LJ::CAPTCHA_IMAGE_PREGEN || 1000; # If there are enough, don't generate any more print "Current count is $count of $MaxItems..."; if ( $count >= $MaxItems ) { print "already have enough.\n"; return; } else { $need = $MaxItems - $count; print "generating $need new image challenges.\n"; } # Load system user for Blob::put() $u = LJ::load_user( "system" ) or die "Couldn't load the system user."; $dbh = LJ::get_db_writer() or die "Failed to get_db_writer()"; # target location my $location = $LJ::CAPTCHA_MOGILEFS ? 'mogile' : 'blob'; # Generate the challenges for ( my $i = 0; $i < $need; $i++ ) { print "Generating image $i..."; $code = gencode( 7 ); ( $png ) = LJ::Captcha::generate_visual( $code ); # Generate the capid + anum print "generating new capid/anum..."; $capid = LJ::alloc_global_counter( 'C' ); die "Couldn't allocate capid" unless $capid; $anum = int( rand 65_535 ); # Insert the blob print "uploading (capid = $capid, anum = $anum)..."; if ($location eq 'mogile') { my $mogfs = LJ::mogclient(); # force load die "Requested to store captchas on MogileFS, but it's not loaded.\n" unless $mogfs; my $fh = $mogfs->new_file("captcha:$capid", 'captcha') or die "Unable to contact MogileFS server for storage.\n"; $fh->print($png); $fh->close or die "Unable to save captcha to MogileFS server: $@\n"; } else { LJ::Blob::put( $u, 'captcha_image', 'png', $capid, $png, \$err ) or die "Error uploading to media server: $err"; } # Insert the captcha into the DB. If it fails for some reason, delete # the just-uploaded file from the media storage system too. print "inserting (code = $code)..."; my $rval = eval { $dbh->do(q{ INSERT INTO captchas( capid, type, location, answer, anum ) VALUES ( ?, 'image', ?, ?, ? ) }, undef, $capid, $location, $code, $anum); }; if ( !$rval || $@ ) { my $err = $@ || $dbh->errstr; if ( $location eq 'mogile' ) { LJ::mogclient()->delete( "captcha:$capid" ); } else { LJ::Blob::delete( $u, 'captcha_image', 'png', $capid ); } die "image captcha insert error on ($capid, $location, $code, $anum): $err"; } print "done.\n"; } print "done. Created $need new image captchas.\n"; return 1; }; $maint{clean_captchas} = sub { my ( $u, # System user $expired, # arrayref of arrayrefs of expired captchas $dbh, # Database handle (writer) $sql, # SQL statement $sth, # Statement handle $count, # Deletion count $err, # Error message reference for Blob::delete calls ); print "-I- Cleaning captchas.\n"; # Find captchas to delete $sql = q{ SELECT capid, type, location FROM captchas WHERE ( issuetime <> 0 AND issuetime < ? ) OR ( userid > 0 AND ( issuetime <> 0 AND issuetime < ? ) ) LIMIT 2500 }; $dbh = LJ::get_db_writer() or die "No master DB handle"; $expired = $dbh->selectall_arrayref( $sql, undef, time() - $ExpireThresNoUser, time() - $ExpireThresUser ); die "selectall_arrayref: $sql: ", $dbh->errstr if $dbh->err; if ( @$expired ) { print "found ", scalar @$expired, " captchas to delete...\n"; } else { print "Done: No captchas to delete.\n"; return; } # Prepare deletion statement $sql = q{ DELETE FROM captchas WHERE capid = ? }; $sth = $dbh->prepare( $sql ); # Fetch system user $u = LJ::load_user( "system" ) or die "Couldn't load the system user."; # Now delete each one from the DB and the media server foreach my $captcha ( @$expired ) { my ( $capid, $type, $location ) = @$captcha; $location ||= 'blob'; print "Deleting captcha $capid ($type, $location)\n"; my $ext = $type eq 'audio' ? 'wav' : 'png'; if ($location eq 'mogile') { my $mogfs = LJ::mogclient(); # force load die "Requested to delete captchas from MogileFS, but it's not loaded.\n" unless $mogfs; $mogfs->delete("captcha:$capid") or die "Unable to delete captcha from MogileFS server for capid = $capid.\n"; } else { LJ::Blob::delete( $u, "captcha_$type", $ext, $capid, \$err ) or die "Failed to delete $type file from media server for ". "capid = $capid: $err"; } $sth->execute( $capid ) or die "execute: $sql ($capid): ", $sth->errstr; $count++; } print "Done: deleted $count expired captchas.\n"; return 1; };