X-Git-Url: http://git.vanrenterghem.biz/git.ikiwiki.info.git/blobdiff_plain/6d402b5a9ae7a46504015ca58ff8f8f2761e8062..93c590e9e8ae2d9ba1705937d11632f8f7e3a786:/doc/todo/require_CAPTCHA_to_edit.mdwn

diff --git a/doc/todo/require_CAPTCHA_to_edit.mdwn b/doc/todo/require_CAPTCHA_to_edit.mdwn
index ec70d2a83..83ba07eb0 100644
--- a/doc/todo/require_CAPTCHA_to_edit.mdwn
+++ b/doc/todo/require_CAPTCHA_to_edit.mdwn
@@ -2,3 +2,326 @@ I don't necessarily trust all OpenID providers to stop bots.  I note that ikiwik
 
 I imagine a plugin that modifies the login screen to use <http://recaptcha.net/>.  You would then be required to fill in the captcha as well as log in in the normal way.
 
+-- [[users/Will]]
+
+> I hate CAPTCHAs with a passion. Someone else is welcome to write such a
+> plugin.
+>
+> If spam via openid (which I have never ever seen yet) becomes
+> a problem, a provider whitelist/blacklist seems like a much nicer
+> solution than a CAPTCHA. --[[Joey]]
+
+>> Apparently there has been openid spam (you can google for it).  But as for
+>> white/black lists, were you thinking of listing the openids, or the content?
+>> Something like the moinmoin global <http://master.moinmo.in/BadContent>
+>> list?
+
+>>> OpenID can be thought of as pushing the problem of determining if
+>>> someone is a human or a spambot back from the openid consumer to the
+>>> openid provider. So, providers that make it possible for spambots to
+>>> use their openids, or that are even set up explicitly for use in
+>>> spamming, would be the ones to block. Or, providers that are known to
+>>> use very good screening for humans would be the ones to allow.
+>>> (Openid delegation makes it a bit harder than just looking at the
+>>> openid url though.) --[[Joey]]
+
+>>>> Well, OpenID only addresses authentication issues, not authorisation issues.
+>>>> Given that it is trivial to set up your own OpenID provider (a full provider, not
+>>>> just a forward to another provider), I can't see a
+>>>> blacklist working in the long term (it would be like blacklisting email).
+>>>> A whitelist might work (it would not be quite as bad as whitelisting email).  In any case,
+>>>> there is now a captcha plugin for those that want it.  It is accessible
+>>>> (there is an audio option) and serves a social purpose along with
+>>>> keeping bots out (the captcha is used to help digitise hard to read
+>>>> words in books for [Carnegie Mellon University](http://www.cs.cmu.edu/) and
+>>>> [The Internet Archive](http://www.archive.org/) ).  Finally, because the actual captcha is outsourced
+>>>> it means that someone else is taking care of keeping it ahead of
+>>>> the bot authors.
+
+Okie - I have a first pass of this.  There are still some issues.
+
+Currently the code verifies the CAPTCHA.  If you get it right then you're fine.
+If you get the CAPTCHA wrong then the current code tells formbuilder that
+one of the fields is invalid.  This stops the login from going through.
+Unfortunately, formbuilder is caching this validity somewhere, and I haven't
+found a way around that yet.  This means that if you get the CAPTCHA
+wrong, it will continue to fail.  You need to load the login page again so
+it doesn't have the error message on the screen, then it'll work again.
+
+> fixed this - updated code is attached.
+
+A second issue is that the OpenID login system resets the 'required' flags
+of all the other fields, so using OpenID will cause the CAPTCHA to be
+ignored.
+
+> This is still not fixed.  I would have thought the following patch would
+> have fixed this second issue, but it doesn't.
+
+(code snipped as a working [[patch]] is below)
+
+>> What seems to be happing here is that the openid plugin defines a
+>> validate hook for openid_url that calls validate(). validate() in turn
+>> redirects the user to the openid server for validation, and exits. If
+>> the openid plugins' validate hook is called before your recaptcha
+>> validator, your code never gets a chance to run. I don't know how to
+>> control the other that FormBuilder validates fields, but the only fix I
+>> can see is to somehow influence that order. 
+>>
+>> Hmm, maybe you need to move your own validation code out of the validate
+>> hook. Instead, just validate the captcha in the formbuilder_setup hook.
+>> The problem with this approach is that if validation fails, you can't
+>> just flag it as invalid and let formbuilder handle that. Instead, you'd
+>> have to hack something in to redisplay the captcha by hand. --[[Joey]]
+
+>>> Fixed this.  I just modified the OpenID plugin to check if the captcha
+>>> succeeded or failed.  Seeing as the OpenID plugin is the one that is
+>>> abusing the normal validate method, I figured it was best to keep
+>>> the fix in the same place.  I also added a config switch so you can set if
+>>> the captcha is needed for OpenID logins. OpenID defaults to ignoring
+>>> the captcha.
+>>> Patch is inline below.
+>>> I think this whole thing is working now.
+
+>>>> Ok, glad it's working. Not thrilled that it needs to modify the
+>>>> openid plugin, especially as I'm not sure if i I will integrate the
+>>>> captcha plugin into mainline. Also because it's not very clean to have
+>>>> the oprnid plugin aware of another plugin like that. I'd like to
+>>>> prusue my idea of not doing the captcha validation in the validate
+>>>> hook.
+
+--- a/IkiWiki/Plugin/openid.pm
++++ b/IkiWiki/Plugin/openid.pm
+@@ -18,6 +18,7 @@ sub getopt () {
+ 	error($@) if $@;
+ 	Getopt::Long::Configure('pass_through');
+ 	GetOptions("openidsignup=s" => \$config{openidsignup});
++	GetOptions("openidneedscaptcha=s" => \$config{openidneedscaptcha});
+ }
+ 
+ sub formbuilder_setup (@) {
+@@ -61,6 +62,7 @@ sub formbuilder_setup (@) {
+ 			# Skip all other required fields in this case.
+ 			foreach my $field ($form->field) {
+ 				next if $field eq "openid_url";
++				next if $config{openidneedscaptcha} && $field eq "recaptcha";
+ 				$form->field(name => $field, required => 0,
+ 					validate => '/.*/');
+ 			}
+@@ -96,6 +98,18 @@ sub validate ($$$;$) {
+ 		}
+ 	}
+ 
++	if ($config{openidneedscaptcha} && defined $form->field("recaptcha")) {
++		foreach my $field ($form->field) {
++			next unless ($field eq "recaptcha");
++			if (! $field->validate) {
++				# if they didn't get the captcha right,
++				# then just claim we validated ok so the
++				# captcha can cause a fail
++				return 1;
++			}
++		}
++	}
++
+ 	my $check_url = $claimed_identity->check_url(
+ 		return_to => IkiWiki::cgiurl(do => "postsignin"),
+ 		trust_root => $config{cgiurl},
+
+
+Instructions
+=====
+
+You need to go to <http://recaptcha.net/api/getkey> and get a key set.
+The keys are added as options.
+
+	reCaptchaPubKey => "LONGPUBLICKEYSTRING",
+	reCaptchaPrivKey => "LONGPRIVATEKEYSTRING",
+
+You can also use "signInSSL" if you're using ssl for your login screen.
+
+
+The following code is just inline.  It will probably not display correctly, and you should just grab it from the page source.
+
+----------
+
+#!/usr/bin/perl
+# Ikiwiki password authentication.
+package IkiWiki::Plugin::recaptcha;
+
+use warnings;
+use strict;
+use IkiWiki 2.00;
+
+sub import {
+	hook(type => "formbuilder_setup", id => "recaptcha", call => \&formbuilder_setup);
+}
+
+sub getopt () {
+	eval q{use Getopt::Long};
+	error($@) if $@;
+	Getopt::Long::Configure('pass_through');
+	GetOptions("reCaptchaPubKey=s" => \$config{reCaptchaPubKey});
+	GetOptions("reCaptchaPrivKey=s" => \$config{reCaptchaPrivKey});
+}
+
+sub formbuilder_setup (@) {
+	my %params=@_;
+
+	my $form=$params{form};
+	my $session=$params{session};
+	my $cgi=$params{cgi};
+	my $pubkey=$config{reCaptchaPubKey};
+	my $privkey=$config{reCaptchaPrivKey};
+	debug("Unknown Public Key.  To use reCAPTCHA you must get an API key from http://recaptcha.net/api/getkey")
+		unless defined $config{reCaptchaPubKey};
+	debug("Unknown Private Key.  To use reCAPTCHA you must get an API key from http://recaptcha.net/api/getkey")
+		unless defined $config{reCaptchaPrivKey};
+	my $tagtextPlain=<<EOTAG;
+		<script type="text/javascript"
+			src="http://api.recaptcha.net/challenge?k=$pubkey">
+		</script>
+
+		<noscript>
+			<iframe src="http://api.recaptcha.net/noscript?k=$pubkey"
+				height="300" width="500" frameborder="0"></iframe><br>
+			<textarea name="recaptcha_challenge_field" rows="3" cols="40"></textarea>
+			<input type="hidden" name="recaptcha_response_field" 
+				value="manual_challenge">
+		</noscript>
+EOTAG
+
+	my $tagtextSSL=<<EOTAGS;
+		<script type="text/javascript"
+			src="https://api-secure.recaptcha.net/challenge?k=$pubkey">
+		</script>
+
+		<noscript>
+			<iframe src="https://api-secure.recaptcha.net/noscript?k=$pubkey"
+				height="300" width="500" frameborder="0"></iframe><br>
+			<textarea name="recaptcha_challenge_field" rows="3" cols="40"></textarea>
+			<input type="hidden" name="recaptcha_response_field" 
+				value="manual_challenge">
+		</noscript>
+EOTAGS
+
+	my $tagtext;
+
+	if ($config{signInSSL}) {
+		$tagtext = $tagtextSSL;
+	} else {
+		$tagtext = $tagtextPlain;
+	}
+	
+	if ($form->title eq "signin") {
+		# Give up if module is unavailable to avoid
+		# needing to depend on it.
+		eval q{use LWP::UserAgent};
+		if ($@) {
+			debug("unable to load LWP::UserAgent, not enabling reCaptcha");
+			return;
+		}
+
+		die("To use reCAPTCHA you must get an API key from http://recaptcha.net/api/getkey")
+			unless $pubkey;
+		die("To use reCAPTCHA you must get an API key from http://recaptcha.net/api/getkey")
+			unless $privkey;
+		die("To use reCAPTCHA you must know the remote IP address")
+			unless $session->remote_addr();
+
+		$form->field(
+			name => "recaptcha",
+			label => "",
+			type => 'static',
+			comment => $tagtext,
+			required => 1,
+			message => "CAPTCHA verification failed",
+		);
+
+		# validate the captcha.
+		if ($form->submitted && $form->submitted eq "Login" &&
+				defined $form->cgi_param("recaptcha_challenge_field") && 
+				length $form->cgi_param("recaptcha_challenge_field") &&
+				defined $form->cgi_param("recaptcha_response_field") && 
+				length $form->cgi_param("recaptcha_response_field")) {
+
+			my $challenge = "invalid";
+			my $response = "invalid";
+			my $result = { is_valid => 0, error => 'recaptcha-not-tested' };
+
+			$form->field(name => "recaptcha",
+				message => "CAPTCHA verification failed",
+				required => 1,
+				validate => sub {
+					if ($challenge ne $form->cgi_param("recaptcha_challenge_field") or
+							$response ne $form->cgi_param("recaptcha_response_field")) {
+						$challenge = $form->cgi_param("recaptcha_challenge_field");
+						$response = $form->cgi_param("recaptcha_response_field");
+						debug("Validating: ".$challenge." ".$response);
+						$result = check_answer($privkey,
+								$session->remote_addr(),
+								$challenge, $response);
+					} else {
+						debug("re-Validating");
+					}
+
+					if ($result->{is_valid}) {
+						debug("valid");
+						return 1;
+					} else {
+						debug("invalid");
+						return 0;
+					}
+				});
+		}
+	}
+}
+
+# The following function is borrowed from
+# Captcha::reCAPTCHA by Andy Armstrong and are under the PERL Artistic License
+
+sub check_answer {
+    my ( $privkey, $remoteip, $challenge, $response ) = @_;
+
+    die
+      "To use reCAPTCHA you must get an API key from http://recaptcha.net/api/getkey"
+      unless $privkey;
+
+    die "For security reasons, you must pass the remote ip to reCAPTCHA"
+      unless $remoteip;
+
+	if (! ($challenge && $response)) {
+		debug("Challenge or response not set!");
+		return { is_valid => 0, error => 'incorrect-captcha-sol' };
+	}
+
+	my $ua = LWP::UserAgent->new();
+
+    my $resp = $ua->post(
+        'http://api-verify.recaptcha.net/verify',
+        {
+            privatekey => $privkey,
+            remoteip   => $remoteip,
+            challenge  => $challenge,
+            response   => $response
+        }
+    );
+
+    if ( $resp->is_success ) {
+        my ( $answer, $message ) = split( /\n/, $resp->content, 2 );
+        if ( $answer =~ /true/ ) {
+            debug("CAPTCHA valid");
+            return { is_valid => 1 };
+        }
+        else {
+            chomp $message;
+            debug("CAPTCHA failed: ".$message);
+            return { is_valid => 0, error => $message };
+        }
+    }
+    else {
+        debug("Unable to contact reCaptcha verification host!");
+        return { is_valid => 0, error => 'recaptcha-not-reachable' };
+    }
+}
+
+1;