[dw-free] offer text captcha as an alternative to reCAPTCHA
[commit: http://hg.dwscoalition.org/dw-free/rev/6888252ac285]
http://bugs.dwscoalition.org/show_bug.cgi?id=3227
Implement textCAPTCHA: Refactors the DW::Captcha module to be able to
support multiple captcha types / implementations. Adds a setting to allow
the captcha type to be set on a per-journal basis. (default will be
textcaptcha rather than the graphical recaptcha)
Patch by
fu.
Files modified:
http://bugs.dwscoalition.org/show_bug.cgi?id=3227
Implement textCAPTCHA: Refactors the DW::Captcha module to be able to
support multiple captcha types / implementations. Adds a setting to allow
the captcha type to be set on a per-journal basis. (default will be
textcaptcha rather than the graphical recaptcha)
Patch by
Files modified:
- bin/upgrading/en.dat
- bin/upgrading/proplists.dat
- cgi-bin/DW/Captcha.pm
- cgi-bin/DW/Captcha/reCAPTCHA.pm
- cgi-bin/DW/Captcha/textCAPTCHA.pm
- cgi-bin/DW/Controller/RPC/TextCAPTCHA.pm
- cgi-bin/DW/Setting/Captcha.pm
- cgi-bin/LJ/Global/Defaults.pm
- cgi-bin/LJ/Talk.pm
- cgi-bin/LJ/User.pm
- cgi-bin/LJ/Widget/SubmitRequest.pm
- doc/config-local.pl.txt
- doc/config-private.pl.txt
- etc/config.pl
- htdocs/create.bml
- htdocs/js/dw/dw-core.js
- htdocs/lostinfo.bml
- htdocs/manage/settings/index.bml
- htdocs/register.bml
- htdocs/stc/textcaptcha.css
- htdocs/support/submit.bml
- t/captcha-textcaptcha.t
- t/captcha.t
- views/textcaptcha-response.tt
- views/textcaptcha-response.tt.text
- views/textcaptcha.tt
- views/textcaptcha.tt.text
--------------------------------------------------------------------------------
diff -r 40433367c052 -r 6888252ac285 bin/upgrading/en.dat
--- a/bin/upgrading/en.dat Wed Feb 15 06:01:15 2012 +0800
+++ b/bin/upgrading/en.dat Sat Feb 18 13:26:01 2012 +0800
@@ -33,6 +33,8 @@
captcha.invalid=Incorrect response in the antispam field. Please try again.
+captcha.loading=Loading anti-spam test...
+
captcha.title=Please fill out the CAPTCHA as an anti-spam measure
cleanhtml.error.markup=( <a [[aopts]]>Error: Irreparable invalid markup in entry. Raw contents behind the cut.</a> )
@@ -2436,6 +2438,14 @@
setting.birthdaydisplay.question=Birthday display options:
+setting.captcha.label=Anti-Spam Type
+
+setting.captcha.option=Commenters will see
+
+setting.captcha.option.select.image=graphical/image anti-spam tests
+
+setting.captcha.option.select.text=text-based anti-spam tests
+
setting.commentcaptcha.label=Anti-Spam
setting.commentcaptcha.option=Show CAPTCHA to
diff -r 40433367c052 -r 6888252ac285 bin/upgrading/proplists.dat
--- a/bin/upgrading/proplists.dat Wed Feb 15 06:01:15 2012 +0800
+++ b/bin/upgrading/proplists.dat Sat Feb 18 13:26:01 2012 +0800
@@ -54,6 +54,14 @@
multihomed: 0
prettyname: Language to browse with
+userproplist.captcha:
+ cldversion: 4
+ datatype: char
+ des: CAPTCHA type to show in the user's journal
+ indexed: 0
+ multihomed: 0
+ prettyname: CAPTCHA type
+
userproplist.city:
cldversion: 4
datatype: char
diff -r 40433367c052 -r 6888252ac285 cgi-bin/DW/Captcha.pm
--- a/cgi-bin/DW/Captcha.pm Wed Feb 15 06:01:15 2012 +0800
+++ b/cgi-bin/DW/Captcha.pm Sat Feb 18 13:26:01 2012 +0800
@@ -7,7 +7,7 @@
# Authors:
# Afuna <coder.dw@afunamatata.com>
#
-# Copyright (c) 2010 by Dreamwidth Studios, LLC.
+# Copyright (c) 2010-2012 by Dreamwidth Studios, LLC.
#
# This program is free software; you may redistribute it and/or modify it under
# the same terms as Perl itself. For a copy of the license, please reference
@@ -20,89 +20,183 @@
=head1 SYNOPSIS
+Here's the simplest method:
+
+ # print out the captcha form fields on a particular page
+ my $captcha = DW::Captcha->new( $page );
+ if ( $captcha->enabled ) {
+ $captcha->print;
+ }
+
+ # elsewhere, process the post
+ if ( $r->did_post ) {
+ my $captcha = DW::Captcha->new( $page, %{$r->post_args} );
+
+ my $captcha_error;
+ push @errors, $captcha_error unless $captcha->validate( err_ref => \$captcha_error );
+ }
+
+
+When using in conjunction with LJ::Widget subclasses, you can just specify the form field names and let the widget handle it:
+
+ LJ::Widget->use_specific_form_fields( post => \%POST, widget => "...", fields => [ DW::Captcha->form_fields ] )
+ if DW::Captcha->enabled( 'create' );
=cut
use strict;
-use warnings;
package DW::Captcha;
-# at some point we may replace this, or try to make this implementation more flexible
-# right now, let's just go with this
+use LJ::ModuleLoader;
-BEGIN {
- my $rv = eval <<USE;
-use Captcha::reCAPTCHA;
-1;
-USE
- warn "NOTE: Captcha::reCAPTCHA was not found.\n"
- unless $rv;
+my @CLASSES = LJ::ModuleLoader->module_subclasses( "DW::Captcha" );
- our $MODULES_INSTALLED = $rv;
+my %impl2class;
+foreach my $class ( @CLASSES ) {
+ eval "use $class";
+ die "Error loading class '$class': $@" if $@;
+ $impl2class{lc $class->name} = $class;
}
# class methods
+=head1 API
+
+=head2 C<< DW::Captcha->new( $implementation, $page, %opts ) >>
+
+Arguments:
+
+=over
+
+=item page - the page we're going to display this CAPTCHA on
+
+=item implementation - which CAPTCHA implementation we'd like to use
+
+=item a hash of additional options, including the request/response from a form post
+
+=back
+
+=cut
+
sub new {
- my ( $class, $type, %opts ) = @_;
+ my ( $class, $page, %opts ) = @_;
+
+ # yes, I really do want to do this rather than $impl{...||$LJ::DEFAULT_CAPTCHA...}
+ # we want to make certain that someone can't force all captchas off
+ # by asking for an invalid captcha type
+ my $impl = $LJ::CAPTCHA_TYPES{delete $opts{want} || ""} || "";
+ my $subclass = $impl2class{$impl};
+ $subclass = $impl2class{$LJ::CAPTCHA_TYPES{$LJ::DEFAULT_CAPTCHA_TYPE}}
+ unless $subclass && $subclass->site_enabled;
my $self = bless {
- type => $type,
- }, $class;
+ page => $page,
+ }, $subclass;
- $self->init_opts( %opts );
+ $self->_init_opts( %opts );
return $self;
}
-sub form_fields { qw( recaptcha_response_field recaptcha_challenge_field ) }
-sub public_key { LJ::conf_test( $LJ::RECAPTCHA{public_key} ) }
-sub private_key { LJ::conf_test( $LJ::RECAPTCHA{private_key} ) }
+# must be implemented by subclasses
+=head2 C<< $class->name >>
-sub site_enabled {
- return 0 unless $DW::Captcha::MODULES_INSTALLED;
- return LJ::is_enabled( 'recaptcha' ) && $LJ::RECAPTCHA{public_key} && $LJ::RECAPTCHA{private_key};
-}
+The name used to refer to this CAPTCHA implementation.
+
+=cut
+
+sub name { return ""; }
+
# object methods
+
+=head2 C<< $captcha->form_fields >>
+
+Returns a list of the form fields expected by the CAPTCHA implementation.
+
+=head2 C<< $captcha->site_enabled >>
+
+Whether CAPTCHA is enabled site-wide. (Specific pages may have CAPTCHA disabled)
+
+=head2 C<< $captcha->print >>
+
+Print the CAPTCHA form fields.
+
+=head2 C<< $captcha->validate( %opts ) >>
+
+Return whether the response for this CAPTCHA was valid.
+
+Arguments:
+
+=over
+
+=item opts - a hash of additional options, including the request/response from a form post
+and an error reference (err_ref) which may contain additional information in case
+the validation failed
+
+=back
+
+=head2 C<< $captcha->enabled( $page ) >>
+
+Whether this CAPTCHA implementation is enabled on this particular page
+(or sitewide if this captcha instance isn't tied to a specific page)
+
+Arguments:
+
+=over
+
+=item page - Optional. A specific page to check
+
+=back
+
+=head2 C<< $captcha->page >>
+
+Return the page that this CAPTCHA instance is going to be used with
+
+=head2 C<< $captcha->challenge >>
+
+Challenge text, provided by the CAPTCHA implementation
+
+=head2 C<< $captcha->response >>
+
+User-provided response text
+
+=cut
+
+# must be implemented by subclasses
+sub form_fields { qw() }
+
+sub site_enabled { return LJ::is_enabled( 'captcha' ) && $_[0]->_implementation_enabled ? 1 : 0 }
+
+# must be implemented by subclasses
+sub _implementation_enabled { return 1; }
+
+
sub print {
my $self = $_[0];
return "" unless $self->enabled;
- my $captcha = Captcha::reCAPTCHA->new;
- my $ret = $captcha->get_options_setter( { theme => 'white' } );
-
- $ret .= "<div class='captcha'>";
-
- $ret .= $captcha->get_html(
- public_key(), # public key
- '', # error (optional)
- $LJ::IS_SSL # page uses ssl
- );
-
- $ret .= "<p>" . BML::ml( 'captcha.accessibility.contact', { email => $LJ::SUPPORT_EMAIL } ) . "</p>";
+ my $ret = "<div class='captcha'>";
+ $ret .= $self->_print;
+ $ret .= "<p style='clear:both'>" . BML::ml( 'captcha.accessibility.contact', { email => $LJ::SUPPORT_EMAIL } ) . "</p>";
$ret .= "</div>";
return $ret;
}
-sub validate {
+# must be implemented by subclasses
+sub _print { return ""; }
+
+sub validate {
my ( $self, %opts ) = @_;
# if disabled, then it's always valid to allow the post to go through
return 1 unless $self->enabled;
- $self->init_opts( %opts );
+ $self->_init_opts( %opts );
my $err_ref = $opts{err_ref};
- my $result;
if ( $self->challenge ) {
- my $captcha = Captcha::reCAPTCHA->new;
- $result = $captcha->check_answer(
- private_key(), $ENV{REMOTE_ADDR},
- $self->challenge, $self->response
- );
-
- return 1 if $result->{is_valid} eq '1';
+ return 1 if $self->_validate;
}
$$err_ref = LJ::Lang::ml( 'captcha.invalid' );
@@ -110,25 +204,30 @@
return 0;
}
-# enabled can be used as either a class or an object method
+# must be implemented by subclasses
+sub _validate { return 0; }
+
sub enabled {
+ my $page;
+ $page = $_[0]->page if ref $_[0];
+ $page ||= $_[1];
- my $type = ref $_[0] ? $_[0]->type : $_[1];
-
- return $type
- ? site_enabled() && $LJ::CAPTCHA_FOR{$type}
- : site_enabled();
+ return $page
+ ? $_[0]->site_enabled() && $LJ::CAPTCHA_FOR{$page}
+ : $_[0]->site_enabled();
}
-sub init_opts {
+# internal method. Used to initialize the challenge and response fields
+# must be implemented by subclasses
+sub _init_opts {
my ( $self, %opts ) = @_;
- $self->{challenge} ||= $opts{recaptcha_challenge_field};
- $self->{response} ||= $opts{recaptcha_response_field};
+ # do something
}
-sub type { return $_[0]->{type} }
+sub page { return $_[0]->{page} }
sub challenge { return $_[0]->{challenge} }
sub response { return $_[0]->{response} }
+
1;
diff -r 40433367c052 -r 6888252ac285 cgi-bin/DW/Captcha/reCAPTCHA.pm
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cgi-bin/DW/Captcha/reCAPTCHA.pm Sat Feb 18 13:26:01 2012 +0800
@@ -0,0 +1,88 @@
+#!/usr/bin/perl
+#
+# DW::Captcha::reCAPTCHA
+#
+# This module handles integration with the reCAPTCHA service
+#
+# Authors:
+# Afuna <coder.dw@afunamatata.com>
+#
+# Copyright (c) 2012 by Dreamwidth Studios, LLC.
+#
+# This program is free software; you may redistribute it and/or modify it under
+# the same terms as Perl itself. For a copy of the license, please reference
+# 'perldoc perlartistic' or 'perldoc perlgpl'.
+#
+
+=head1 NAME
+
+DW::Captcha::reCAPTCHA - This module handles integration with the reCAPTCHA service
+
+=head1 SYNOPSIS
+
+=cut
+
+use strict;
+
+package DW::Captcha::reCAPTCHA;
+use base 'DW::Captcha';
+
+BEGIN {
+ my $rv = eval <<USE;
+use Captcha::reCAPTCHA;
+1;
+USE
+ warn "NOTE: Captcha::reCAPTCHA was not found.\n"
+ unless $rv;
+
+ our $MODULES_INSTALLED = $rv;
+}
+
+
+# implemented as overrides for the base class
+
+
+# class methods
+sub name { return "recaptcha" }
+
+# object methods
+sub form_fields { qw( recaptcha_response_field recaptcha_challenge_field ) }
+sub _implementation_enabled {
+ return 0 unless $DW::Captcha::reCAPTCHA::MODULES_INSTALLED;
+ return LJ::is_enabled( 'captcha', 'recaptcha' ) && _public_key() && _private_key() ? 1 : 0;
+}
+
+sub _print {
+ my $captcha = Captcha::reCAPTCHA->new;
+ return $captcha->get_options_setter( { theme => 'white' } ) .
+ $captcha->get_html(
+ _public_key(), # public key
+ '', # error (optional)
+ $LJ::IS_SSL # page uses ssl
+ );
+}
+
+sub _validate {
+ my $self = $_[0];
+
+ my $captcha = Captcha::reCAPTCHA->new;
+ my $result = $captcha->check_answer(
+ _private_key(), $ENV{REMOTE_ADDR},
+ $self->challenge, $self->response
+ );
+
+ return 1 if $result->{is_valid} eq '1';
+}
+
+sub _init_opts {
+ my ( $self, %opts ) = @_;
+
+ $self->{challenge} ||= $opts{recaptcha_challenge_field};
+ $self->{response} ||= $opts{recaptcha_response_field};
+}
+
+# recaptcha-specific methods
+sub _public_key { LJ::conf_test( $LJ::RECAPTCHA{public_key} ) }
+sub _private_key { LJ::conf_test( $LJ::RECAPTCHA{private_key} ) }
+
+1;
diff -r 40433367c052 -r 6888252ac285 cgi-bin/DW/Captcha/textCAPTCHA.pm
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cgi-bin/DW/Captcha/textCAPTCHA.pm Sat Feb 18 13:26:01 2012 +0800
@@ -0,0 +1,237 @@
+#!/usr/bin/perl
+#
+# DW::Captcha::textCAPTCHA
+#
+# This module handles integration with the textCAPTCHA service
+#
+# Authors:
+# Afuna <coder.dw@afunamatata.com>
+#
+# Copyright (c) 2012 by Dreamwidth Studios, LLC.
+#
+# This program is free software; you may redistribute it and/or modify it under
+# the same terms as Perl itself. For a copy of the license, please reference
+# 'perldoc perlartistic' or 'perldoc perlgpl'.
+#
+
+=head1 NAME
+
+DW::Captcha::textCAPTCHA - This module handles integration with the textCAPTCHA service
+
+=head1 SYNOPSIS
+
+=cut
+
+use strict;
+
+package DW::Captcha::textCAPTCHA;
+use base 'DW::Captcha';
+
+use XML::Simple;
+use Digest::MD5 ();
+
+
+# implemented as overrides for the base class
+
+
+# class methods
+sub name { return "textcaptcha" }
+
+# object methods
+sub form_fields { qw( textcaptcha_challenge textcaptcha_response textcaptcha_response_noscript textcaptcha_chalauth ) }
+sub _implementation_enabled {
+ return LJ::is_enabled( 'captcha', 'textcaptcha' ) && _api_key() ? 1 : 0;
+}
+
+# this prints out the js/iframe to load the captcha (but not the captcha itself)
+sub _print {
+ my $self = $_[0];
+
+ # form_auth remains the same throughout the lifetime of the request
+ # so we can just call this here instead of passing it in from the external form
+ my $auth = LJ::form_auth( 1 );
+
+ # we can't use need_res, alas, because we're printing this inline
+ # after the <head> has already been printed
+ # FIXME: remove check when we get rid of the old library
+ my $loading_text = LJ::Lang::ml( "captcha.loading" );
+ my $captcha_load = $LJ::ACTIVE_RES_GROUP && $LJ::ACTIVE_RES_GROUP eq "jquery"
+ ? qq!
+<script type="text/javascript">
+jQuery(function(jq){
+ jq("#textcaptcha_container").html("$loading_text")
+ .load(jq.endpoint("captcha") + "/$auth");
+});
+</script>
+ ! : qq!
+<script type="text/javascript">
+ \$("textcaptcha_container").innerHTML = "$loading_text";
+ HTTPReq.getJSON({
+ "url": LiveJournal.getAjaxUrl("captcha") + "/$auth.json",
+ "method": "GET",
+ "onError": LiveJournal.ajaxError,
+ "onData": function(data) {\$("textcaptcha_container").innerHTML = data.captcha}
+ })
+</script>
+ !;
+
+ my $response_label = LJ::Lang::ml( "/textcaptcha-response.tt.response.user.label" );
+
+ # putting it in noscript so that we don't load it the page unnecessarily if we actually have JS
+ return qq{
+<div id="textcaptcha_container" aria-live="assertive" style="line-height: 1.2em">
+ $captcha_load
+<noscript><iframe src="$LJ::SITEROOT/captcha/text/$auth" style="width:100%;height:4em" id="textcaptcha_fallback"></iframe>
+<label for="textcaptcha_response_noscript">$response_label</label> <input type="text" maxlength="255" autocomplete="off" value="" name="textcaptcha_response_noscript" class="text" id="textcaptcha_response_noscript" size="50" />
+</noscript>
+</div>};
+}
+
+sub _validate {
+ my $self = $_[0];
+ return DW::Captcha::textCAPTCHA::Logic::check_answer( $self->challenge, $self->response, $self->form_auth, $self->captcha_auth );
+}
+
+sub _init_opts {
+ my ( $self, %opts ) = @_;
+
+ # rather than having a lot of ifs/elses here to extract multiple keys
+ # when we're pulling via BML vs via a controller, etc
+ # let's just pull directly from the request
+ my $r = DW::Request->get;
+ my $post_args = $r->post_args if $r;
+
+ if ( $post_args ) {
+ if ( my $response_noscript = $post_args->{textcaptcha_response_noscript} ) {
+ my %parsed = DW::Captcha::textCAPTCHA::Logic::from_form_string( $response_noscript );
+ $self->{$_} ||= $parsed{$_} foreach qw( challenge response form_auth captcha_auth );
+ } else {
+ # allow multiple values
+ $self->{challenge} ||= [ $post_args->get_all( "textcaptcha_challenge" ) ];
+
+ # just allow the user to submit one
+ $self->{response} ||= $post_args->{textcaptcha_response};
+
+ # assume we need the form auth
+ $self->{form_auth} ||= $post_args->{lj_form_auth};
+
+ $self->{captcha_auth} ||= $post_args->{textcaptcha_chalauth};
+ }
+ }
+}
+
+=head1 C<< textCAPTCHA-specific methods >>
+
+=cut
+
+# textcaptcha-specific methods
+sub _api_key { LJ::conf_test( $LJ::TEXTCAPTCHA{api_key} ) }
+
+=head2 C<< $captcha->form_auth >>
+
+Generic form auth. Ties this captcha to a specific form instance.
+
+=cut
+
+=head2 C<< $captcha->captcha_auth >>
+
+Additional auth for this captcha. Enforces time limit and single use.
+
+=cut
+
+sub form_auth { return $_[0]->{form_auth} }
+sub captcha_auth { return $_[0]->{captcha_auth} }
+
+package DW::Captcha::textCAPTCHA::Logic;
+
+# this is an internal class which shouldn't be called directly by anything else
+# (except maybe tests)
+
+sub fetch {
+ my ( $class ) = $_[0];
+
+ my $ua = LJ::get_useragent( role => 'textcaptcha', timeout => $LJ::TEXTCAPTCHA{timeout} );
+ $ua->agent("$LJ::SITENAME ($LJ::ADMIN_EMAIL; captcha request)");
+ my $res = $ua->get( "http://api.textcaptcha.com/" . DW::Captcha::textCAPTCHA::_api_key() );
+ return $res && $res->is_success ? $res->content : "";
+}
+
+# arguments:
+# * xml string containing the captcha question as a plain string
+# and answer, or answers, as an MD5 hash
+# * the form auth which we can use to tie this captcha to a particular instance
+
+# returns a hashref containing data suitable for use within the form:
+# * the question to display
+# * answers (salted)
+# * captcha auth
+sub form_data {
+ my ( $captcha_data_string, $auth ) = @_;
+ my $captcha = eval { XML::Simple::XMLin( $captcha_data_string, ForceArray => [ 'answer' ] ); };
+
+ # get the timestamp
+ my $secret = LJ::get_secret( (split( /:/, $auth ))[1] );
+ my @salted_answers = map { Digest::MD5::md5_hex( $auth . $secret . $_ ) } @{$captcha->{answer}};
+
+ return {
+ question => $captcha->{question},
+ answers => \@salted_answers,
+ chal => LJ::challenge_generate( 900 ), # 15 minute token
+ };
+}
+
+# arguments:
+# * valid responses to the captcha
+# * the user's response
+# * form auth
+# * captcha auth
+sub check_answer {
+ my ( $form_responses, $user_response, $form_auth, $captcha_auth ) = @_;
+
+ # all forms we use captcha with should have had a corresponding lj_form_auth
+ # but just in case we miss a spot (though we really shouldn't) let's cut this short
+ # also cut short if we don't provide the captcha-specific auth
+ return 0 unless $form_auth && $captcha_auth;
+
+ my $chal_opts = {};
+ return 0 unless LJ::challenge_check( $captcha_auth, $chal_opts );
+
+ my $secret = LJ::get_secret( ( split( /:/, $form_auth ))[1] );
+
+ my $user_answer = Digest::MD5::md5_hex( LJ::trim( lc $user_response ) );
+ my $check_answer = Digest::MD5::md5_hex( $form_auth . $secret . $user_answer );
+
+ foreach ( @$form_responses ) {
+ return 1 if $_ eq $check_answer;
+ }
+
+ return 0;
+}
+
+# concatenate all relevant values into one string
+sub to_form_string {
+ my $self = $_[0];
+
+ return join( "::",
+ ( $self->form_auth,
+ $self->captcha_auth,
+ $self->response,
+ join( "::", @{$self->challenge||[]} )
+ )
+ );
+}
+
+# return a hash
+sub from_form_string {
+ my ( $string ) = @_;
+
+ my ( $form_auth, $captcha_auth, $response, @challenges ) = split "::", $string;
+ return (
+ form_auth => $form_auth,
+ captcha_auth => $captcha_auth,
+ response => $response,
+ challenge => \@challenges
+ );
+}
+
+1;
diff -r 40433367c052 -r 6888252ac285 cgi-bin/DW/Controller/RPC/TextCAPTCHA.pm
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cgi-bin/DW/Controller/RPC/TextCAPTCHA.pm Sat Feb 18 13:26:01 2012 +0800
@@ -0,0 +1,73 @@
+#!/usr/bin/perl
+#
+# DW::Controller::TextCAPTCHA
+#
+# AJAX endpoint that returns a textCAPTCHA instance
+# May also be loaded on a page
+#
+# Author:
+# Afuna <coder.dw@afunamatata.com>
+#
+# Copyright (c) 2012 by Dreamwidth Studios, LLC.
+#
+# This program is free software; you may redistribute it and/or modify it under
+# the same terms as Perl itself. For a copy of the license, please reference
+# 'perldoc perlartistic' or 'perldoc perlgpl'.
+#
+
+package DW::Controller::RPC::TextCAPTCHA;
+
+use strict;
+
+use DW::Routing;
+use JSON;
+use DW::Captcha::textCAPTCHA;
+
+DW::Routing->register_regex( "/__rpc_captcha/(.*)", \&captcha_handler, app => 1, user => 1, format => 'html', formats => [qw( html json )] );
+DW::Routing->register_regex( "/captcha/text/(.*)", \&iframe_captcha_handler, app => 1, format => 'html' );
+
+# loaded inline into the page using JS
+sub captcha_handler {
+ my ( $call_opts, $auth ) = @_;
+
+ my $from_textcaptcha = DW::Captcha::textCAPTCHA::Logic->fetch;
+ my ( $captcha ) = DW::Captcha::textCAPTCHA::Logic::form_data( $from_textcaptcha, $auth );
+
+ if ( $call_opts->format eq "json" ) {
+ # json format is for the old JS library
+ my $captcha_html = DW::Template->template_string( 'textcaptcha.tt', { captcha => $captcha }, { fragment => 1 } );
+
+ my $r = DW::Request->get;
+ $r->print( objToJson( { captcha => $captcha_html } ) );
+ return $r->OK;
+ } else {
+ return DW::Template->render_template( 'textcaptcha.tt', { captcha => $captcha }, { fragment => 1 } );
+ }
+}
+
+# fallback for the no-js case
+sub iframe_captcha_handler {
+ my ( $call_opts, $auth ) = @_;
+
+ my $error;
+ my $r = DW::Request->get;
+ if ( $r->did_post ) {
+ my $captcha_object = DW::Captcha->new( undef, %{$r->post_args} );
+
+ # we don't check whether this is true or not (we'd end up expiring the captcha's auth if we did)
+ return DW::Template->render_template( 'textcaptcha-response.tt', { response => DW::Captcha::textCAPTCHA::Logic::to_form_string( $captcha_object ) }, { fragment => 1 } );
+ }
+
+ my $from_textcaptcha = DW::Captcha::textCAPTCHA::Logic->fetch;
+ my ( $captcha ) = DW::Captcha::textCAPTCHA::Logic::form_data( $from_textcaptcha, $auth );
+
+ return DW::Template->render_template( 'textcaptcha.tt', {
+ handle_submit => 1,
+
+ captcha => $captcha,
+ form_auth => $auth,
+ error => $error
+ }, { fragment => 1 } );
+}
+
+1;
diff -r 40433367c052 -r 6888252ac285 cgi-bin/DW/Setting/Captcha.pm
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/cgi-bin/DW/Setting/Captcha.pm Sat Feb 18 13:26:01 2012 +0800
@@ -0,0 +1,71 @@
+#!/usr/bin/perl
+#
+# DW::Setting::Captcha
+#
+# LJ::Setting module for choosing the captcha type to display on this journal
+#
+# Authors:
+# Afuna <coder.dw@afunamatata.com>
+#
+# Copyright (c) 2012 by Dreamwidth Studios, LLC.
+#
+# This program is free software; you may redistribute it and/or modify it under
+# the same terms as Perl itself. For a copy of the license, please reference
+# 'perldoc perlartistic' or 'perldoc perlgpl'.
+#
+
+package DW::Setting::Captcha;
+use base 'LJ::Setting';
+use strict;
+
+use DW::Captcha;
+
+sub should_render {
+ my ( $class, $u ) = @_;
+ return $u->is_identity ? 0 : 1;
+}
+
+sub label {
+ my $class = shift;
+ return $class->ml( 'setting.captcha.label' );
+}
+
+sub option {
+ my ( $class, $u, $errs, $args ) = @_;
+ my $key = $class->pkgkey;
+
+ my $captcha_type = $class->get_arg( $args, "captcha" ) || $u->captcha_type;
+
+ my @opts = (
+ "T" => $class->ml( "setting.captcha.option.select.text" ),
+ "I" => $class->ml( "setting.captcha.option.select.image" ),
+ );
+
+ my $ret;
+ $ret .= "<label for='${key}captcha'>";
+ $ret .= $class->ml( 'setting.captcha.option' );
+ $ret .= "</label> ";
+
+ $ret .= LJ::html_select( { name => "${key}captcha",
+ id => "${key}captcha",
+ selected => $captcha_type },
+ @opts );
+
+ my $errdiv = $class->errdiv( $errs, "captcha" );
+ $ret .= "<br />$errdiv" if $errdiv;
+
+ return $ret;
+}
+
+sub save {
+ my ( $class, $u, $args ) = @_;
+
+ my $val = $class->get_arg( $args, "captcha" );
+ $val = undef unless $val =~ /^[IT]$/;
+
+ $u->captcha_type( $val );
+
+ return 1;
+}
+
+1;
diff -r 40433367c052 -r 6888252ac285 cgi-bin/LJ/Global/Defaults.pm
--- a/cgi-bin/LJ/Global/Defaults.pm Wed Feb 15 06:01:15 2012 +0800
+++ b/cgi-bin/LJ/Global/Defaults.pm Sat Feb 18 13:26:01 2012 +0800
@@ -358,6 +358,14 @@
"js/hoverIntent.js" => "js/hoverIntent.minified.js",
"js/tooltip.js" => "js/tooltip.min.js",
) unless defined %LJ::MINIFY;
+
+ # mapping of captcha type to specific desired implementation
+ %CAPTCHA_TYPES = (
+ "T" => "textcaptcha", # "T" is for text
+ "I" => "recaptcha", # "I" is for image
+ ) unless defined %CAPTCHA_TYPES;
+ $DEFAULT_CAPTCHA_TYPE ||= "T";
+
}
diff -r 40433367c052 -r 6888252ac285 cgi-bin/LJ/Talk.pm
--- a/cgi-bin/LJ/Talk.pm Wed Feb 15 06:01:15 2012 +0800
+++ b/cgi-bin/LJ/Talk.pm Sat Feb 18 13:26:01 2012 +0800
@@ -2032,7 +2032,7 @@
# Display captcha challenge if over rate limits.
if ( $opts->{do_captcha} ) {
- my $captcha = DW::Captcha->new;
+ my $captcha = DW::Captcha->new( undef, want => $journalu->captcha_type );
$ret .= $captcha->print;
}
@@ -3665,8 +3665,10 @@
# unixify line-endings
$form->{'body'} =~ s/\r\n/\n/g;
+ # FIXME: remove when we no longer support BML
+ $form->{textcaptcha_challenge} = [ split /\0/, $form->{textcaptcha_challenge} ];
+
# now check for UTF-8 correctness, it must hold
-
return $err->("<?badinput?>") unless LJ::text_in($form);
$init->{unknown8bit} = 0;
@@ -3775,10 +3777,11 @@
##
## 1. Check rate by remote user and by IP (for anonymous user)
##
- if ( DW::Captcha->enabled( 'anonpost' ) || DW::Captcha->enabled( 'authpost' ) ) {
+ my $captcha = DW::Captcha->new;
+ if ( $captcha->enabled( 'anonpost' ) || $captcha->enabled( 'authpost' ) ) {
return 1 unless LJ::Talk::Post::check_rate( $commenter, $journal );
}
- if ( DW::Captcha->enabled( 'anonpost' ) && $anon_commenter) {
+ if ( $captcha->enabled( 'anonpost' ) && $anon_commenter) {
return 1 if LJ::sysban_check( 'talk_ip_test', LJ::get_remote_ip() );
}
@@ -3819,8 +3822,8 @@
## 4. Global (site) settings
## See if they have any tags or URLs in the comment's body
##
- if ( DW::Captcha->enabled( 'comment_html_auth' )
- || ( DW::Captcha->enabled( 'comment_html_anon' ) && $anon_commenter))
+ if ( $captcha->enabled( 'comment_html_auth' )
+ || ( $captcha->enabled( 'comment_html_anon' ) && $anon_commenter))
{
if ($body =~ /<[a-z]/i) {
# strip white-listed bare tags w/o attributes,
diff -r 40433367c052 -r 6888252ac285 cgi-bin/LJ/User.pm
--- a/cgi-bin/LJ/User.pm Wed Feb 15 06:01:15 2012 +0800
+++ b/cgi-bin/LJ/User.pm Sat Feb 18 13:26:01 2012 +0800
@@ -2187,6 +2187,16 @@
return $_[0]->get_cap( 'viewmailqueue' ) ? 1 : 0;
}
+sub captcha_type {
+ my $u = $_[0];
+
+ if ( defined $_[1] ) {
+ $u->set_prop( captcha => $_[1] );
+ }
+
+ return $_[1] || $u->prop( 'captcha' ) || $LJ::DEFAULT_CAPTCHA_TYPE;
+}
+
sub clear_prop {
my ($u, $prop) = @_;
$u->set_prop($prop, undef);
diff -r 40433367c052 -r 6888252ac285 cgi-bin/LJ/Widget/SubmitRequest.pm
--- a/cgi-bin/LJ/Widget/SubmitRequest.pm Wed Feb 15 06:01:15 2012 +0800
+++ b/cgi-bin/LJ/Widget/SubmitRequest.pm Sat Feb 18 13:26:01 2012 +0800
@@ -219,11 +219,5 @@
return ('spid' => $spid);
}
-sub error_list {
- my ($class, @errors) = @_;
- return unless @errors;
-
- $class->error($_) foreach @errors;
-}
1;
diff -r 40433367c052 -r 6888252ac285 doc/config-local.pl.txt
--- a/doc/config-local.pl.txt Wed Feb 15 06:01:15 2012 +0800
+++ b/doc/config-local.pl.txt Sat Feb 18 13:26:01 2012 +0800
@@ -56,6 +56,11 @@
private_key => $DW::PRIVATE::RECAPTCHA{private_key},
);
+ # setup textcaptcha
+ %TEXTCAPTCHA = (
+ api_key => $DW::PRIVATE::TEXTCAPTCHA{api_key},
+ );
+
# If enabled, disable people coming in over Tor exits from using various parts of the site.
$USE_TOR_CONFIGS = 0;
diff -r 40433367c052 -r 6888252ac285 doc/config-private.pl.txt
--- a/doc/config-private.pl.txt Wed Feb 15 06:01:15 2012 +0800
+++ b/doc/config-private.pl.txt Sat Feb 18 13:26:01 2012 +0800
@@ -169,6 +169,13 @@
# public_key => ,
# private_key => ,
#);
+
+ #%TEXTCAPTCHA = (
+ # # this works for testing purposes.
+ # # sign up at the textcaptcha website for a key for production use
+ # api_key => "demo",
+ # timeout => 10,
+ #);
}
1;
diff -r 40433367c052 -r 6888252ac285 etc/config.pl
--- a/etc/config.pl Wed Feb 15 06:01:15 2012 +0800
+++ b/etc/config.pl Sat Feb 18 13:26:01 2012 +0800
@@ -141,6 +141,7 @@
adult_content => 0,
blockwatch => 1,
'community-logins' => 0,
+ captcha => 0,
directory => 0,
esn_archive => 1,
eventlogrecord => 1,
@@ -164,6 +165,16 @@
# ref $_[0] && defined $_[0]->{sitename} &&
# $_[0]->{sitename} eq 'LiveJournal' ? 1 : 0 };
+ # disable the recaptcha module, but keep textcaptcha enabled
+ # you'd probably want to edit $LJ::DEFAULT_CAPTCHA_TYPE in this case
+ #$DISABLED{captcha} = sub {
+ # my $module = $_[0];
+ # return 1 if $module eq "recaptcha";
+ # return 0 if $module eq "textcaptcha";
+ # return 0;
+ #};
+
+
# turn $SERVER_DOWN on while you do any maintenance
$SERVER_TOTALLY_DOWN = 0;
$SERVER_DOWN = 0;
@@ -838,6 +849,7 @@
# on why they're banned
# $BLOCKED_ANON_URI = '';
+ # pages where we want to see captcha
%CAPTCHA_FOR = (
create => 0,
lostinfo => 1,
diff -r 40433367c052 -r 6888252ac285 htdocs/create.bml
--- a/htdocs/create.bml Wed Feb 15 06:01:15 2012 +0800
+++ b/htdocs/create.bml Sat Feb 18 13:26:01 2012 +0800
@@ -39,9 +39,11 @@
my $code;
my $rate_ok;
if ( LJ::did_post() ) {
- LJ::Widget->use_specific_form_fields( post => \%POST, widget => "CreateAccount", fields => [ DW::Captcha->form_fields ] )
- if DW::Captcha->enabled( 'create' );
- %from_post = LJ::Widget->handle_post( \%POST, ( 'CreateAccount' ) );
+ my $post_args = DW::Request->get->post_args;
+ my $captcha = DW::Captcha->new( 'create' );
+ LJ::Widget->use_specific_form_fields( post => $post_args, widget => "CreateAccount", fields => [ $captcha->form_fields ] )
+ if $captcha->enabled( 'create' );
+ %from_post = LJ::Widget->handle_post( $post_args, ( 'CreateAccount' ) );
} else {
# we always need the code, because it might contain paid time
$code = LJ::trim( $GET{code} );
diff -r 40433367c052 -r 6888252ac285 htdocs/js/dw/dw-core.js
--- a/htdocs/js/dw/dw-core.js Wed Feb 15 06:01:15 2012 +0800
+++ b/htdocs/js/dw/dw-core.js Sat Feb 18 13:26:01 2012 +0800
@@ -54,6 +54,10 @@
image: function() { return $("<img>", { src: $.throbber.src } ) }
};
+$.endpoint = function(action){
+ return Site && Site.currentJournal ? "/" + Site.currentJournal + "/__rpc_" + action : "/__rpc_" + action;
+};
+
// position is an optional first argument
$.fn.throbber = function(position, jqxhr) {
var $this = $(this);
diff -r 40433367c052 -r 6888252ac285 htdocs/lostinfo.bml
--- a/htdocs/lostinfo.bml Wed Feb 15 06:01:15 2012 +0800
+++ b/htdocs/lostinfo.bml Sat Feb 18 13:26:01 2012 +0800
@@ -24,9 +24,7 @@
LJ::set_active_crumb('lostinfo');
- my %captcha_fields;
- @captcha_fields{DW::Captcha->form_fields} = @POST{DW::Captcha->form_fields};
- my $captcha = DW::Captcha->new( 'lostinfo', %captcha_fields );
+ my $captcha = DW::Captcha->new( 'lostinfo', %POST );
my $captcha_error;
@@ -38,7 +36,7 @@
if $captcha->enabled;
$ret .= "<form action='lostinfo' method='post'>\n";
-
+ $ret .= LJ::form_auth();
$ret .= $captcha->print;
$ret .= "<?h1 $ML{'.lostpassword.title'} h1?>\n";
@@ -81,6 +79,7 @@
}
# we have a post action
+ return LJ::bad_input( LJ::Lang::ml( "error.invalidform" ) ) unless LJ::check_form_auth();
# note: some lostinfo_do.bml translation strings are used below
# because the code was once in lostinfo_do.bml, but later
diff -r 40433367c052 -r 6888252ac285 htdocs/manage/settings/index.bml
--- a/htdocs/manage/settings/index.bml Wed Feb 15 06:01:15 2012 +0800
+++ b/htdocs/manage/settings/index.bml Sat Feb 18 13:26:01 2012 +0800
@@ -133,6 +133,7 @@
LJ::Setting::EnableComments
LJ::Setting::CommentScreening
LJ::Setting::CommentCaptcha
+ DW::Setting::Captcha
LJ::Setting::CommentIP
LJ::Setting::Display::BanUsers
DW::Setting::AllowVgiftsFrom
diff -r 40433367c052 -r 6888252ac285 htdocs/register.bml
--- a/htdocs/register.bml Wed Feb 15 06:01:15 2012 +0800
+++ b/htdocs/register.bml Sat Feb 18 13:26:01 2012 +0800
@@ -104,6 +104,7 @@
my $captcha = DW::Captcha->new( 'validate_openid', %POST );
if ( $captcha->response ) {
+ return LJ::bad_input( "error.invalidform" ) unless LJ::check_form_auth() && $captcha->enabled;
my $err_ref;
return LJ::bad_input( $err_ref )
unless $captcha->validate( err_ref => \$err_ref );
@@ -111,6 +112,7 @@
my $ret = "<?h1 $ML{'captcha.title'} h1?>";
$ret .= "<form method='POST' action='$LJ::SITEROOT/register'><input type='hidden' name='qs' value='$qs'>";
$ret .= $captcha->print;
+ $ret .= LJ::form_auth() if $captcha->enabled;
$ret .= "<input type='submit' value='$ML{'.form.submit'}' ></form>";
return $ret;
}
diff -r 40433367c052 -r 6888252ac285 htdocs/stc/textcaptcha.css
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/htdocs/stc/textcaptcha.css Sat Feb 18 13:26:01 2012 +0800
@@ -0,0 +1,6 @@
+body { font-family: sans-serif; padding: 0; margin: 0; }
+p { line-height: 1.5em; margin: 0; padding: 0;}
+textarea { height: 2.5em; width: 50%; display: block; }
+
+form, .textcaptcha {padding: 0 !important; margin: 0;}
+.
diff -r 40433367c052 -r 6888252ac285 htdocs/support/submit.bml
--- a/htdocs/support/submit.bml Wed Feb 15 06:01:15 2012 +0800
+++ b/htdocs/support/submit.bml Sat Feb 18 13:26:01 2012 +0800
@@ -20,9 +20,12 @@
use strict;
LJ::set_active_crumb('supportsubmit');
- LJ::Widget->use_specific_form_fields( post => \%POST, widget => "SubmitRequest_Support", fields => [ DW::Captcha->form_fields ] );
- return LJ::Widget->handle_post_and_render(\%POST, 'LJ::Widget::SubmitRequest::Support',
+ my $captcha = DW::Captcha->new();
+ LJ::Widget->use_specific_form_fields( post => \%POST, widget => "SubmitRequest_Support", fields => [ $captcha->form_fields ] );
+ my $ret = LJ::Widget->handle_post_and_render(\%POST, 'LJ::Widget::SubmitRequest::Support',
'category' => $GET{category}, 'post' => LJ::Widget::SubmitRequest::Support->post_fields(\%POST));
+ my @errors = LJ::Widget->error_list;
+ return @errors ? LJ::error_list( @errors ) . $ret : $ret;
}
_code?>
<=body
diff -r 40433367c052 -r 6888252ac285 t/captcha-textcaptcha.t
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/t/captcha-textcaptcha.t Sat Feb 18 13:26:01 2012 +0800
@@ -0,0 +1,129 @@
+# -*-perl-*-
+
+use strict;
+use Test::More tests => 21;
+use lib "$ENV{LJHOME}/cgi-bin";
+require 'ljlib.pl';
+
+use DW::Captcha;
+use XML::Simple;
+
+my $fakeanswers_single = {
+ question => 'The white bank is what colour?',
+ answer => 'd508fe45cecaf653904a0e774084bb5c',
+};
+
+my $fakeanswers_multiple = {
+ question => 'If I have twelve monkeys, how many monkeys do I have?',
+ answer => [ "c20ad4d76fe97759aa27a0c99bff6710", # 12
+ "15f6f8dc036519d7fe15b39338f6e5db", # twelve
+ ],
+};
+
+# convenience method to generate and handle answers for the captcha
+# to make multiple inputs to the test easier to understand
+sub _run_test {
+ local $Test::Builder::Level = $Test::Builder::Level + 2;
+
+ my ( $content, $auth, $answer, $testmsg, $fail ) = @_;
+
+ subtest "generating and testing captcha" => sub {
+ # generate new captcha auth for each, because we can't reuse captcha on this form
+ # note: we can reuse the form auth! might have a need to pull in alternate captcha for same form
+ my $captcha = DW::Captcha::textCAPTCHA::Logic::form_data( $content, $auth );
+ my $checked = DW::Captcha::textCAPTCHA::Logic::check_answer( $captcha->{answers}, $answer, $auth, $captcha->{chal} );
+
+ $fail ? ok( ! $checked, $testmsg ) : ok( $checked, $testmsg );
+ }
+}
+
+note( "single answer" );
+{
+ LJ::start_request();
+ my $content = XML::Simple::XMLout( $fakeanswers_single, NoAttr => 1 );
+ my $auth = LJ::form_auth( 1 );
+ my $captcha = DW::Captcha::textCAPTCHA::Logic::form_data( $content, $auth );
+
+ # we want the question to be the same
+ # but we know the answer will be different -- we don't know and don't care what it will be though
+ is( $captcha->{question}, $fakeanswers_single->{question}, "got back the question for use in a form" );
+ isnt( $captcha->{answers}->[0], $fakeanswers_single->{answer}, "got back an answer for use in a form (which does not look like what we put in)" );
+
+ isnt( $captcha->{chal}, $auth, "Form auth and captcha auth are not the same" );
+
+ my $test_captcha = sub {
+ my ( $answer, $msg, %opts ) = @_;
+ return _run_test( $content, $auth, $answer, $msg, $opts{fail} ? 1 : 0 );
+ };
+
+ # now validate user responses
+ $test_captcha->( "blue" , "completely incorrect", fail => 1 );
+ $test_captcha->( "white" , "correct" );
+ $test_captcha->( "WHITE" , "correct (caps)" );
+ $test_captcha->( "white ", "correct (whitespace)" );
+
+ LJ::start_request();
+ $captcha = DW::Captcha::textCAPTCHA::Logic::form_data( $content, $auth );
+ ok( ! DW::Captcha::textCAPTCHA::Logic::check_answer( $captcha->{answers}, "white", LJ::form_auth( 1 ), $captcha->{chal} ), "incorrect (auth; tried to submit captcha on a different form?)" );
+}
+
+note( "multiple valid answers" );
+{
+ LJ::start_request();
+ my $content = XML::Simple::XMLout( $fakeanswers_multiple, NoAttr => 1 );
+ my $auth = LJ::form_auth( 1 );
+ my $captcha = DW::Captcha::textCAPTCHA::Logic::form_data( $content, $auth );
+
+ my %original_answers = map { $_ => 1 } @{$fakeanswers_multiple->{answer}};
+ is( $captcha->{question}, $fakeanswers_multiple->{question}, "got back the question for use in a form" );
+
+ foreach ( @{ $captcha->{answers} } ) {
+ ok( !$original_answers{$_}, "one ofs multiple answers for use in a form (which does not look like what we put in)" );
+ }
+
+ my $test_captcha = sub {
+ my ( $answer, $msg, %opts ) = @_;
+ return _run_test( $content, $auth, $answer, $msg, $opts{fail} ? 1 : 0 );
+ };
+
+ # now validate user responses
+ $test_captcha->( "12" , "correct ('12' is one of the valid choices)" );
+ $test_captcha->( "twelve" , "correct ('twelve' is another of the valid choices)" );
+ $test_captcha->( "a dozen", "incorrect ('a dozen' is not one of the valid choices)", fail => 1 );
+};
+
+note( "no form auth passed in" );
+{
+ my $content = XML::Simple::XMLout( $fakeanswers_single, NoAttr => 1 );
+ my $captcha = DW::Captcha::textCAPTCHA::Logic::form_data( $content, "" );
+ my $captcha_auth = $captcha->{chal};
+
+ # we want the question to be the same
+ # but we know the answer will be different -- we don't know and don't care what it will be though
+ is( $captcha->{question}, $fakeanswers_single->{question}, "got back the question for use in a form" );
+ isnt( $captcha->{answers}->[0], $fakeanswers_single->{answer}, "got back an answer for use in a form (which does not look like what we put in)" );
+
+ # now validate user response
+ ok( ! DW::Captcha::textCAPTCHA::Logic::check_answer( $captcha->{answers}, "white", "", $captcha_auth ), "correct answer, but we have no auth" );
+};
+
+note( "tried to reuse captcha + form_auth" );
+{
+ LJ::start_request();
+ my $content = XML::Simple::XMLout( $fakeanswers_single, NoAttr => 1 );
+ my $auth = LJ::form_auth( 1 );
+ my $captcha = DW::Captcha::textCAPTCHA::Logic::form_data( $content, $auth );
+ my $captcha_auth = $captcha->{chal};
+
+ # we want the question to be the same
+ # but we know the answer will be different -- we don't know and don't care what it will be though
+ is( $captcha->{question}, $fakeanswers_single->{question}, "got back the question for use in a form" );
+ isnt( $captcha->{answers}->[0], $fakeanswers_single->{answer}, "got back an answer for use in a form (which does not look like what we put in)" );
+
+ # now validate user response
+ ok( DW::Captcha::textCAPTCHA::Logic::check_answer( $captcha->{answers}, "white", $auth, $captcha_auth ), "correct" );
+
+ # whoo captcha succeded! let's try to reuse it
+ LJ::start_request();
+ ok( ! DW::Captcha::textCAPTCHA::Logic::check_answer( $captcha->{answers}, "white", $auth, $captcha_auth ), "tried to reuse captcha results" );
+};
diff -r 40433367c052 -r 6888252ac285 t/captcha.t
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/t/captcha.t Sat Feb 18 13:26:01 2012 +0800
@@ -0,0 +1,68 @@
+# -*-perl-*-
+
+use strict;
+use Test::More tests => 14;
+use lib "$ENV{LJHOME}/cgi-bin";
+require 'ljlib.pl';
+
+# override for the sake of the test
+%LJ::CAPTCHA_FOR = (
+ testpage => 1,
+ nocaptchapage => 0,
+);
+
+note( "disabled captcha for a specific page" );
+{
+ my $captcha = DW::Captcha->new( "nocaptchapage" );
+ ok( ! $captcha->enabled, "Captcha is not enabled for nocaptchapage" );
+}
+
+note( "check captcha is enabled" );
+{
+ my $captcha = DW::Captcha->new( "testpage" );
+ ok( $captcha->enabled, "Captcha is enabled for testpage" );
+}
+
+note( "check various implementations are loaded okay" );
+{
+ my $default = $LJ::CAPTCHA_TYPES{$LJ::DEFAULT_CAPTCHA_TYPE};
+
+ ok( DW::Captcha->site_enabled, "Captcha is enabled site-wide" );
+ my $captcha = DW::Captcha->new( 'testpage' );
+ is( $captcha->name, $default, "Use default captcha implementation" );
+
+
+ $captcha = DW::Captcha->new( 'testpage', want => 'I' );
+ is( $captcha->name, "recaptcha", "Using reCAPTCHA" );
+
+ # can also be done using DW::Captcha::reCAPTCHA->site_enabled
+ # but technically we shouldn't be worrying about module names
+ ok( $captcha->site_enabled, "reCAPTCHA is enabled and configured on this site" );
+
+
+ $captcha = DW::Captcha->new( 'testpage', want => 'T' );
+ is( $captcha->name, "textcaptcha", "Using textCAPTCHA" );
+ ok( $captcha->site_enabled, "textCAPTCHA is enabled and configured on this site" );
+
+ $captcha = DW::Captcha->new( 'testpage', want => 'abc' );
+ is( $captcha->name, $default, "not a valid captcha implementation, so used default" );
+ ok( $captcha->site_enabled, "not a valid captcha implementation, so used default to make sure we still get captcha" );
+}
+
+note( "user tries to use a disabled captcha type" );
+{
+ local %LJ::DISABLED = ( captcha => sub {
+ my $module = $_[0];
+ return 0 if $module eq "recaptcha";
+ return 1 if $module eq "textcaptcha";
+ } );
+ local $LJ::DEFAULT_CAPTCHA_TYPE = "I";
+
+ my $captcha = DW::Captcha->new( "testpage", want => "I" ); # image
+ is( $captcha->name, "recaptcha", "want recaptcha, everything is fine" );
+ ok( $captcha->site_enabled, "recaptcha was enabled" );
+
+ my $captcha = DW::Captcha->new( "testpage", want => "T" ); # text
+ is( $captcha->name, "recaptcha", "wanted textcaptcha, but it's not enabled so use recaptcha instead" );
+ ok( $captcha->site_enabled, "recaptcha (our fallback) is enabled" );
+}
diff -r 40433367c052 -r 6888252ac285 views/textcaptcha-response.tt
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/views/textcaptcha-response.tt Sat Feb 18 13:26:01 2012 +0800
@@ -0,0 +1,21 @@
+[%# textcaptcha-response.tt
+
+Displays captcha-related info the user needs to paste into their main form
+
+Authors:
+ Afuna <coder.dw@afunamatata.com>
+
+This program is free software; you may redistribute it and/or modify it under
+the same terms as Perl itself. For a copy of the license, please reference
+'perldoc perlartistic' or 'perldoc perlgpl'.
+-%]
+
+[% dw.need_res( 'stc/textcaptcha.css' ) %]
+<div class="response">
+[%- label ='.response.iframe.label' | ml -%]
+[%- form.textarea( label = label
+ name = "captcha_response"
+ id = "captcha_response"
+ value = response
+) -%]
+</div>
diff -r 40433367c052 -r 6888252ac285 views/textcaptcha-response.tt.text
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/views/textcaptcha-response.tt.text Sat Feb 18 13:26:01 2012 +0800
@@ -0,0 +1,4 @@
+;; -*- coding: utf-8 -*-
+.response.iframe.label=Captcha Response (copy this)
+
+.response.user.label=Captcha Response (paste here)
diff -r 40433367c052 -r 6888252ac285 views/textcaptcha.tt
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/views/textcaptcha.tt Sat Feb 18 13:26:01 2012 +0800
@@ -0,0 +1,61 @@
+[%# textcaptcha.tt
+
+Displays an instance of textCAPTCHA, to be pulled into a page
+
+Authors:
+ Afuna <coder.dw@afunamatata.com>
+
+This program is free software; you may redistribute it and/or modify it under
+the same terms as Perl itself. For a copy of the license, please reference
+'perldoc perlartistic' or 'perldoc perlgpl'.
+-%]
+
+[%- IF handle_submit -%]
+ [% dw.need_res( 'stc/textcaptcha.css' ) %]
+
+ <form method='POST'>
+ [%- IF error -%]
+ <p>[% error %]</p>
+ [%- ELSE -%]
+ <p>[% '.nojs.instructions' | ml %]</p>
+ [%- END -%]
+[%- END -%]
+
+<div class='textcaptcha' style='padding: 0.5em 0'>
+[%- IF captcha.question -%]
+ [%- form.textbox( label = captcha.question
+ id = "textcaptcha_response"
+ name = "textcaptcha_response"
+
+ maxlength = "255"
+ size = "50"
+
+ autocomplete = "off"
+ ) -%]
+
+ [%- FOREACH answer = captcha.answers;
+ form.hidden(
+ name = "textcaptcha_challenge"
+ value = answer
+ );
+ END -%]
+
+ [%- form.hidden(
+ name = "textcaptcha_chalauth"
+ value = captcha.chal
+ ) -%]
+
+ [%- IF handle_submit -%]
+ [%- form.hidden(
+ name = "lj_form_auth"
+ value = form_auth
+ ) -%]
+ [%- form.submit() -%]
+ </form>
+ [%- END -%]
+
+[%- ELSE -%]
+[%- ".error.noquestion" | ml -%]
+[%- END -%]
+
+</div>
diff -r 40433367c052 -r 6888252ac285 views/textcaptcha.tt.text
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/views/textcaptcha.tt.text Sat Feb 18 13:26:01 2012 +0800
@@ -0,0 +1,5 @@
+;; -*- coding: utf-8 -*-
+
+.error.noquestion=Sorry, the anti-spam test is not available right now. Please wait and try again.
+
+.nojs.instructions=Copy the result to the "Captcha Response" field
--------------------------------------------------------------------------------

no subject
This is awesome!