iguana: The Tilley Hemp Hat (Default)
[personal profile] iguana
Another code post, but don't worry, I have things to talk about for my regular update too.

I've been trying to set up a captcha field - specifically Google's ReCaptcha - on a not-as-old-as-you'd-think Pylons project which uses ToscaWidgets for rendering forms. I'd link you to Pylon's page as well but it merged with Repoze and apparently maintenance is hard. I'm pretty frowny about this but that's for another day.

On top of that, the 0.8 release of tw.recaptcha, which is supposed to make it easy to add recaptcha into ToscaWidget forms, didn't work, due to two things:

* You have to give it the remote IP in the Validator's constructor, while the Validator needs to be created before you know that.
* Formencode (apparently) validates every form twice for some reason, and of course this doesn't work on things like captcha where the correct answer changes per request.

Anyway, someone kindly left a comment on the wiki page for tw.recaptcha and although Google managed to mangle it by forcing WikiMarkup on people, I managed to reconstruct it into something that works. Below is a diff that can be applied to the 0.8 release in Pypi and makes it approximately 20% cooler.

--- ./tw.recaptcha-0.8/tw/recaptcha/validator.py        2008-06-01 12:07:51.000000000 +0100
+++ ./tw-recaptcha-f846368854fe/tw/recaptcha/validator.py       2011-10-21 14:51:51.000000000 +0100
@@ -5,6 +5,8 @@
 import gettext
 _ = gettext.gettext

 class ReCaptchaValidator(FancyValidator):
     """
     @see formencode.validators.FieldsMatch
@@ -28,6 +30,7 @@
         self.remote_ip = remote_ip
         self.field_names = ['recaptcha_challenge_field',
                             'recaptcha_response_field']
+        self.submitted_pairs = {}

     def validate_partial(self, field_dict, state):
         for name in self.field_names:
@@ -38,13 +41,21 @@
     def validate_python(self, field_dict, state):
         challenge = field_dict['recaptcha_challenge_field']
         response = field_dict['recaptcha_response_field']
+        if self.submitted_pairs.has_key(challenge):
+            #log.debug("This pair has already been submitted by this class!!!! CAN'T DO THAT!!!")
+            #log.debug(self.submitted_pairs[challenge])
+            return self.submitted_pairs[challenge][1]
         if response == '' or challenge == '':
             error = Invalid(self.message('missing', state), field_dict, state)
             error.error_dict = {'recaptcha_response_field':'Missing value'}
             raise error
+        if callable(self.remote_ip):
+            remote_ip = self.remote_ip()
+        else:
+            remote_ip = self.remote_ip
         params = urllib.urlencode({
             'privatekey': self.private_key,
-            'remoteip' : self.remote_ip,
+            'remoteip' : remote_ip,
             'challenge': challenge,
             'response' : response,
             })
@@ -61,7 +72,10 @@
         httpresp.close();
         return_code = return_values[0]
         if not return_code == "true":
+            self.submitted_pairs[challenge] = (response, False)
             error = Invalid(self.message('incorrect', state), field_dict, state)
             error.error_dict = {'recaptcha_response_field':self.message('incorrect', state)}
             raise error
+        self.submitted_pairs[challenge] = (response, True)
         return True


To me it looks a bit like this introduces a memory leak since we're having to store every previous attempted captcha, but since this particular project gets restarted fairly often to perform log rotation, I'm not going to worry about that at the moment.

For completeness, the form and validator back on my side of things looks like this:

class NewUserSchema(formencode.Schema):
    name = formencode.validators.UnicodeString(min=5, max=64)
    # ... [don't add 'recaptcha_response_field' here specifically]

    chained_validators = [tw.recaptcha.validator.ReCaptchaValidator(config['recaptcha.private.key'], request.environ.get("X_FORWARDED_FOR", request.environ["REMOTE_ADDR"]))]
    filter_extra_fields = False
    allow_extra_fields = True
    filter_extra_fields = False 

apply_form = twf.TableForm('apply_form', action='apply', validator=NewUserSchema, children=[
    twf.TextField('name', label_text="Your name", validator=twf.validators.NotEmpty),
    # ...
    tw.recaptcha.widgets.ReCaptchaWidget('recaptcha_response_field', public_key=config['recaptcha.public.key']),
], submit_text='Register')

July 2023

S M T W T F S
      1
2345678
9101112131415
16171819202122
2324252627 28 29
3031     

Most Popular Tags

Expand Cut Tags

No cut tags