Sunday, July 10, 2011

How Add-ons Mozilla does ReCaptcha

Firefox add-ons register

Code comments are potentially mine.

1. def register in apps/users/views.py
@anonymous_csrf
def register(request):
    if request.user.is_authenticated():
        messages.info(request, _("You are already logged in to an account."))
        form = None
    elif request.method == 'POST':

        form = forms.UserRegisterForm(request.POST) # Always have recaptcha

        if form.is_valid(): # is_valid() does all the form clean()
            ... [save form stuff] ...
    else:
        form = forms.UserRegisterForm()
    return jingo.render(request, 'users/register.html', {'form': form, })

2. UserRegisterForm has ReCaptchaField. File: apps/users/forms.py
import captcha.fields

class UserRegisterForm(happyforms.ModelForm, PasswordMixin):
    passwords ...  
               
    recaptcha = captcha.fields.ReCaptchaField()

    ... irrelevent stuff ...

3. captcha.fields.ReCaptchaField() in zamboni/vendors (not shown on zamboni github), but it's on Mozilla's django-recaptcha
from django.conf import settings
from django import forms
from django.utils.encoding import smart_unicode
from django.utils.translation import ugettext_lazy as _

from recaptcha.client import captcha

from captcha.widgets import ReCaptcha

class ReCaptchaField(forms.CharField):

    default_error_messages = {
        'captcha_invalid': _(u'Invalid captcha')
    }

    def __init__(self, *args, **kwargs):
        self.widget = ReCaptcha
        self.required = True
        super(ReCaptchaField, self).__init__(*args, **kwargs)

    def clean(self, values):
        super(ReCaptchaField, self).clean(values[1])
        recaptcha_challenge_value = smart_unicode(values[0])
        recaptcha_response_value = smart_unicode(values[1])
        check_captcha = captcha.submit(recaptcha_challenge_value,
            recaptcha_response_value, settings.RECAPTCHA_PRIVATE_KEY, {})
        if not check_captcha.is_valid:
            raise forms.util.ValidationError(
                    self.error_messages['captcha_invalid'])
        return values[0]

Which, btw is exactly what I have for my ReCaptchaField. The ReCaptcha widget will ultimately introduce an in-body javascript.
Click to read more about ReCaptcha and In-Line Javascript / CSP

So the only difference in reCaptcha is how it's displayed on the html page. Let's investigate.

4. Register page template: apps/users/templates/users/register.html, taken from step 1 views.py
{% block js %}{% include("amo/recaptcha_js.html") %}{% endblock %}
...
{% if settings.RECAPTCHA_PRIVATE_KEY %}
    {{ recaptcha(form) }}
{% else %}
    <p>
       Welcome Robots, ReCaptcha has been disabled for your convenience.
       Spam at Wil.
     </p>
{% endif %}
The apps/amo/templates/amo/recaptcha_js.html has:
{% if request.user.is_anonymous() %}
  <script type="text/javascript" src="{{ settings.RECAPTCHA_URL }}"></script>
{% endif %}
where
# in settings.py
RECAPTCHA_PUBLIC_KEY = "blah"
RECAPTCHA_PRIVATE_KEY = "blah"
RECAPTCHA_URL = ('https://www.google.com/recaptcha/api/challenge?k=%s' %
                 RECAPTCHA_PUBLIC_KEY)

Unless you have the private key (which bots don't), you can see the recaptcha form.


5. def recaptcha() in apps/amo/helpers.py
Read about the inclusion_tag
@register.inclusion_tag('amo/recaptcha.html')
@jinja2.contextfunction
def recaptcha(context, form):
    d = dict(context.items())
    d.update(form=form)
    return d

6. recaptcha.html lives in apps/amo/templates/amo/recaptcha.hhtml"
{% from 'includes/forms.html' import required %}
<label for="recaptcha_response_field">
  {{ _('Are you human?') }} {{ required() }}
</label>
{% trans %}
  <p>
    Please enter <strong>both words</strong> below,
    <strong>separated by a space</strong>.
  </p>
  <p>
    If this is hard to read, you can
    <a href="#" id="recaptcha_different">try different words</a> or
    <a href="#" id="recaptcha_audio">listen to something</a> instead.
  </p>
{% endtrans %}
<div id="recaptcha_image"></div>
<p>
  <input type="text" name="recaptcha_response_field"
         id="recaptcha_response_field" size="30" />
</p>
<p><a href="#" id="recaptcha_help">{{ _("What's this?") }}</a></p>
{{ form.recaptcha.errors }}

7. div ids link to function in javascript here: media/js/zamboni/users.js
// Recaptcha
var RecaptchaOptions = { theme : 'custom' };

$('#recaptcha_different').click(function(e) {
    e.preventDefault();
    Recaptcha.reload();
});

$('#recaptcha_audio').click(function(e) {
    e.preventDefault();
    Recaptcha.switch_type('audio');
});

$('#recaptcha_help').click(function(e) {
    e.preventDefault();
    Recaptcha.showhelp();
});
These Recaptcha's functions are defined in Google's recaptcha.

No comments:

Post a Comment