web commit by http://willu.myopenid.com/: Fix CAPTCHA code so you can actually try...
[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 a todo.
34
35 Instructions
36 =====
37
38 You need to go to <http://recaptcha.net/api/getkey> and get a key set.
39 The keys are added as options.
40
41         reCaptchaPubKey => "LONGPUBLICKEYSTRING",
42         reCaptchaPrivKey => "LONGPRIVATEKEYSTRING",
43
44 You can also use "signInSSL" if you're using ssl for your login screen.
45
46
47 The following code is just inline.  It will probably not display correctly, and you should just grab it from the page source.
48
49 ----------
50
51 #!/usr/bin/perl
52 # Ikiwiki password authentication.
53 package IkiWiki::Plugin::recaptcha;
54
55 use warnings;
56 use strict;
57 use IkiWiki 2.00;
58
59 sub import { #{{{
60         hook(type => "formbuilder_setup", id => "recaptcha", call => \&formbuilder_setup);
61 } # }}}
62
63 sub getopt () { #{{{
64         eval q{use Getopt::Long};
65         error($@) if $@;
66         Getopt::Long::Configure('pass_through');
67         GetOptions("reCaptchaPubKey=s" => \$config{reCaptchaPubKey});
68         GetOptions("reCaptchaPrivKey=s" => \$config{reCaptchaPrivKey});
69 } #}}}
70
71 sub formbuilder_setup (@) { #{{{
72         my %params=@_;
73
74         my $form=$params{form};
75         my $session=$params{session};
76         my $cgi=$params{cgi};
77         my $pubkey=$config{reCaptchaPubKey};
78         my $privkey=$config{reCaptchaPrivKey};
79         debug("Unknown Public Key.  To use reCAPTCHA you must get an API key from http://recaptcha.net/api/getkey")
80                 unless defined $config{reCaptchaPubKey};
81         debug("Unknown Private Key.  To use reCAPTCHA you must get an API key from http://recaptcha.net/api/getkey")
82                 unless defined $config{reCaptchaPrivKey};
83         my $tagtextPlain=<<EOTAG;
84                 <script type="text/javascript"
85                         src="http://api.recaptcha.net/challenge?k=$pubkey">
86                 </script>
87
88                 <noscript>
89                         <iframe src="http://api.recaptcha.net/noscript?k=$pubkey"
90                                 height="300" width="500" frameborder="0"></iframe><br>
91                         <textarea name="recaptcha_challenge_field" rows="3" cols="40"></textarea>
92                         <input type="hidden" name="recaptcha_response_field" 
93                                 value="manual_challenge">
94                 </noscript>
95 EOTAG
96
97         my $tagtextSSL=<<EOTAGS;
98                 <script type="text/javascript"
99                         src="https://api-secure.recaptcha.net/challenge?k=$pubkey">
100                 </script>
101
102                 <noscript>
103                         <iframe src="https://api-secure.recaptcha.net/noscript?k=$pubkey"
104                                 height="300" width="500" frameborder="0"></iframe><br>
105                         <textarea name="recaptcha_challenge_field" rows="3" cols="40"></textarea>
106                         <input type="hidden" name="recaptcha_response_field" 
107                                 value="manual_challenge">
108                 </noscript>
109 EOTAGS
110
111         my $tagtext;
112
113         if ($config{signInSSL}) {
114                 $tagtext = $tagtextSSL;
115         } else {
116                 $tagtext = $tagtextPlain;
117         }
118         
119         if ($form->title eq "signin") {
120                 # Give up if module is unavailable to avoid
121                 # needing to depend on it.
122                 eval q{use LWP::UserAgent};
123                 if ($@) {
124                         debug("unable to load LWP::UserAgent, not enabling reCaptcha");
125                         return;
126                 }
127
128                 die("To use reCAPTCHA you must get an API key from http://recaptcha.net/api/getkey")
129                         unless $pubkey;
130                 die("To use reCAPTCHA you must get an API key from http://recaptcha.net/api/getkey")
131                         unless $privkey;
132                 die("To use reCAPTCHA you must know the remote IP address")
133                         unless $session->remote_addr();
134
135                 $form->field(
136                         name => "recaptcha",
137                         label => "",
138                         type => 'static',
139                         comment => $tagtext,
140                         required => 1,
141                         message => "CAPTCHA verification failed",
142                 );
143
144                 # validate the captcha.
145                 if ($form->submitted && $form->submitted eq "Login" &&
146                                 defined $form->cgi_param("recaptcha_challenge_field") && 
147                                 length $form->cgi_param("recaptcha_challenge_field") &&
148                                 defined $form->cgi_param("recaptcha_response_field") && 
149                                 length $form->cgi_param("recaptcha_response_field")) {
150
151                         my $challenge = "invalid";
152                         my $response = "invalid";
153                         my $result = { is_valid => 0, error => 'recaptcha-not-tested' };
154
155                         $form->field(name => "recaptcha",
156                                 message => "CAPTCHA verification failed",
157                                 required => 1,
158                                 validate => sub {
159                                         if ($challenge ne $form->cgi_param("recaptcha_challenge_field") or
160                                                         $response ne $form->cgi_param("recaptcha_response_field")) {
161                                                 $challenge = $form->cgi_param("recaptcha_challenge_field");
162                                                 $response = $form->cgi_param("recaptcha_response_field");
163                                                 debug("Validating: ".$challenge." ".$response);
164                                                 $result = check_answer($privkey,
165                                                                 $session->remote_addr(),
166                                                                 $challenge, $response);
167                                         } else {
168                                                 debug("re-Validating");
169                                         }
170
171                                         if ($result->{is_valid}) {
172                                                 debug("valid");
173                                                 return 1;
174                                         } else {
175                                                 debug("invalid");
176                                                 return 0;
177                                         }
178                                 });
179                 }
180         }
181 } # }}}
182
183 # The following function is borrowed from
184 # Captcha::reCAPTCHA by Andy Armstrong and are under the PERL Artistic License
185
186 sub check_answer {
187     my ( $privkey, $remoteip, $challenge, $response ) = @_;
188
189     die
190       "To use reCAPTCHA you must get an API key from http://recaptcha.net/api/getkey"
191       unless $privkey;
192
193     die "For security reasons, you must pass the remote ip to reCAPTCHA"
194       unless $remoteip;
195
196         if (! ($challenge && $response)) {
197                 debug("Challenge or response not set!");
198                 return { is_valid => 0, error => 'incorrect-captcha-sol' };
199         }
200
201         my $ua = LWP::UserAgent->new();
202
203     my $resp = $ua->post(
204         'http://api-verify.recaptcha.net/verify',
205         {
206             privatekey => $privkey,
207             remoteip   => $remoteip,
208             challenge  => $challenge,
209             response   => $response
210         }
211     );
212
213     if ( $resp->is_success ) {
214         my ( $answer, $message ) = split( /\n/, $resp->content, 2 );
215         if ( $answer =~ /true/ ) {
216             debug("CAPTCHA valid");
217             return { is_valid => 1 };
218         }
219         else {
220             chomp $message;
221             debug("CAPTCHA failed: ".$message);
222             return { is_valid => 0, error => $message };
223         }
224     }
225     else {
226         debug("Unable to contact reCaptcha verification host!");
227         return { is_valid => 0, error => 'recaptcha-not-reachable' };
228     }
229 }
230
231 1;
232