web commit by http://willu.myopenid.com/: Some info on tracking down the remaining...
[ikiwiki] / doc / todo / require_CAPTCHA_to_edit.mdwn
1 I don't necessarily trust all OpenID providers to stop bots.  I note that ikiwiki allows [[banned_users]], and that there are other todos such as [[todo/openid_user_filtering]] that would extend this.  However, it might be nice to have a CAPTCHA system.
2
3 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.
4
5 > I hate CAPTCHAs with a passion. Someone else is welcome to write such a
6 > plugin.
7 >
8 > If spam via openid (which I have never ever seen yet) becomes
9 > a problem, a provider whitelist/blacklist seems like a much nicer
10 > solution than a CAPTCHA. --[[Joey]]
11
12 >> Apparently there has been openid spam (you can google for it).  But as for
13 >> white/black lists, were you thinking of listing the openids, or the content?
14 >> Something like the moinmoin global <http://master.moinmo.in/BadContent>
15 >> list?
16
17 Okie - I have a first pass of this.  There are still some issues.
18
19 Currently the code verifies the CAPTCHA.  If you get it right then you're fine.
20 If you get the CAPTCHA wrong then the current code tells formbuilder that
21 one of the fields is invalid.  This stops the login from going through.
22 Unfortunately, formbuilder is caching this validity somewhere, and I haven't
23 found a way around that yet.  This means that if you get the CAPTCHA
24 wrong, it will continue to fail.  You need to load the login page again so
25 it doesn't have the error message on the screen, then it'll work again.
26
27 > fixed this - updated code is attached.
28
29 A second issue is that the OpenID login system resets the 'required' flags
30 of all the other fields, so using OpenID will cause the CAPTCHA to be
31 ignored.
32
33 > This is still not fixed.  I would have thought the following patch would
34 > have fixed this second issue, but it doesn't.
35
36 --- a/IkiWiki/Plugin/openid.pm
37 +++ b/IkiWiki/Plugin/openid.pm
38 @@ -61,6 +61,7 @@ sub formbuilder_setup (@) { #{{{
39                         # Skip all other required fields in this case.
40                         foreach my $field ($form->field) {
41                                 next if $field eq "openid_url";
42 +                               next if $field eq "recaptcha";
43                                 $form->field(name => $field, required => 0,
44                                         validate => '/.*/');
45                         }
46
47
48 Instructions
49 =====
50
51 You need to go to <http://recaptcha.net/api/getkey> and get a key set.
52 The keys are added as options.
53
54         reCaptchaPubKey => "LONGPUBLICKEYSTRING",
55         reCaptchaPrivKey => "LONGPRIVATEKEYSTRING",
56
57 You can also use "signInSSL" if you're using ssl for your login screen.
58
59
60 The following code is just inline.  It will probably not display correctly, and you should just grab it from the page source.
61
62 ----------
63
64 #!/usr/bin/perl
65 # Ikiwiki password authentication.
66 package IkiWiki::Plugin::recaptcha;
67
68 use warnings;
69 use strict;
70 use IkiWiki 2.00;
71
72 sub import { #{{{
73         hook(type => "formbuilder_setup", id => "recaptcha", call => \&formbuilder_setup);
74 } # }}}
75
76 sub getopt () { #{{{
77         eval q{use Getopt::Long};
78         error($@) if $@;
79         Getopt::Long::Configure('pass_through');
80         GetOptions("reCaptchaPubKey=s" => \$config{reCaptchaPubKey});
81         GetOptions("reCaptchaPrivKey=s" => \$config{reCaptchaPrivKey});
82 } #}}}
83
84 sub formbuilder_setup (@) { #{{{
85         my %params=@_;
86
87         my $form=$params{form};
88         my $session=$params{session};
89         my $cgi=$params{cgi};
90         my $pubkey=$config{reCaptchaPubKey};
91         my $privkey=$config{reCaptchaPrivKey};
92         debug("Unknown Public Key.  To use reCAPTCHA you must get an API key from http://recaptcha.net/api/getkey")
93                 unless defined $config{reCaptchaPubKey};
94         debug("Unknown Private Key.  To use reCAPTCHA you must get an API key from http://recaptcha.net/api/getkey")
95                 unless defined $config{reCaptchaPrivKey};
96         my $tagtextPlain=<<EOTAG;
97                 <script type="text/javascript"
98                         src="http://api.recaptcha.net/challenge?k=$pubkey">
99                 </script>
100
101                 <noscript>
102                         <iframe src="http://api.recaptcha.net/noscript?k=$pubkey"
103                                 height="300" width="500" frameborder="0"></iframe><br>
104                         <textarea name="recaptcha_challenge_field" rows="3" cols="40"></textarea>
105                         <input type="hidden" name="recaptcha_response_field" 
106                                 value="manual_challenge">
107                 </noscript>
108 EOTAG
109
110         my $tagtextSSL=<<EOTAGS;
111                 <script type="text/javascript"
112                         src="https://api-secure.recaptcha.net/challenge?k=$pubkey">
113                 </script>
114
115                 <noscript>
116                         <iframe src="https://api-secure.recaptcha.net/noscript?k=$pubkey"
117                                 height="300" width="500" frameborder="0"></iframe><br>
118                         <textarea name="recaptcha_challenge_field" rows="3" cols="40"></textarea>
119                         <input type="hidden" name="recaptcha_response_field" 
120                                 value="manual_challenge">
121                 </noscript>
122 EOTAGS
123
124         my $tagtext;
125
126         if ($config{signInSSL}) {
127                 $tagtext = $tagtextSSL;
128         } else {
129                 $tagtext = $tagtextPlain;
130         }
131         
132         if ($form->title eq "signin") {
133                 # Give up if module is unavailable to avoid
134                 # needing to depend on it.
135                 eval q{use LWP::UserAgent};
136                 if ($@) {
137                         debug("unable to load LWP::UserAgent, not enabling reCaptcha");
138                         return;
139                 }
140
141                 die("To use reCAPTCHA you must get an API key from http://recaptcha.net/api/getkey")
142                         unless $pubkey;
143                 die("To use reCAPTCHA you must get an API key from http://recaptcha.net/api/getkey")
144                         unless $privkey;
145                 die("To use reCAPTCHA you must know the remote IP address")
146                         unless $session->remote_addr();
147
148                 $form->field(
149                         name => "recaptcha",
150                         label => "",
151                         type => 'static',
152                         comment => $tagtext,
153                         required => 1,
154                         message => "CAPTCHA verification failed",
155                 );
156
157                 # validate the captcha.
158                 if ($form->submitted && $form->submitted eq "Login" &&
159                                 defined $form->cgi_param("recaptcha_challenge_field") && 
160                                 length $form->cgi_param("recaptcha_challenge_field") &&
161                                 defined $form->cgi_param("recaptcha_response_field") && 
162                                 length $form->cgi_param("recaptcha_response_field")) {
163
164                         my $challenge = "invalid";
165                         my $response = "invalid";
166                         my $result = { is_valid => 0, error => 'recaptcha-not-tested' };
167
168                         $form->field(name => "recaptcha",
169                                 message => "CAPTCHA verification failed",
170                                 required => 1,
171                                 validate => sub {
172                                         if ($challenge ne $form->cgi_param("recaptcha_challenge_field") or
173                                                         $response ne $form->cgi_param("recaptcha_response_field")) {
174                                                 $challenge = $form->cgi_param("recaptcha_challenge_field");
175                                                 $response = $form->cgi_param("recaptcha_response_field");
176                                                 debug("Validating: ".$challenge." ".$response);
177                                                 $result = check_answer($privkey,
178                                                                 $session->remote_addr(),
179                                                                 $challenge, $response);
180                                         } else {
181                                                 debug("re-Validating");
182                                         }
183
184                                         if ($result->{is_valid}) {
185                                                 debug("valid");
186                                                 return 1;
187                                         } else {
188                                                 debug("invalid");
189                                                 return 0;
190                                         }
191                                 });
192                 }
193         }
194 } # }}}
195
196 # The following function is borrowed from
197 # Captcha::reCAPTCHA by Andy Armstrong and are under the PERL Artistic License
198
199 sub check_answer {
200     my ( $privkey, $remoteip, $challenge, $response ) = @_;
201
202     die
203       "To use reCAPTCHA you must get an API key from http://recaptcha.net/api/getkey"
204       unless $privkey;
205
206     die "For security reasons, you must pass the remote ip to reCAPTCHA"
207       unless $remoteip;
208
209         if (! ($challenge && $response)) {
210                 debug("Challenge or response not set!");
211                 return { is_valid => 0, error => 'incorrect-captcha-sol' };
212         }
213
214         my $ua = LWP::UserAgent->new();
215
216     my $resp = $ua->post(
217         'http://api-verify.recaptcha.net/verify',
218         {
219             privatekey => $privkey,
220             remoteip   => $remoteip,
221             challenge  => $challenge,
222             response   => $response
223         }
224     );
225
226     if ( $resp->is_success ) {
227         my ( $answer, $message ) = split( /\n/, $resp->content, 2 );
228         if ( $answer =~ /true/ ) {
229             debug("CAPTCHA valid");
230             return { is_valid => 1 };
231         }
232         else {
233             chomp $message;
234             debug("CAPTCHA failed: ".$message);
235             return { is_valid => 0, error => $message };
236         }
237     }
238     else {
239         debug("Unable to contact reCaptcha verification host!");
240         return { is_valid => 0, error => 'recaptcha-not-reachable' };
241     }
242 }
243
244 1;
245