Captcha Plugin Architecture

This document contains design notes for the plone.app.discussion Captcha plugin architecture.

Introduction

When a Captcha plugin (e.g. plone.formwidget.captcha or plone.formwidget.recaptcha) is installed, plone.app.discussion extends the comment form with a Captcha field/widget and a Captcha validator.

The form extender and validator are only registered if there is a plugin installed that claims to provide the “plone.app.discussion-captcha” feature in its configure.zcml file:

<configure
    xmlns:meta="http://namespaces.zope.org/meta"
    xmlns:zcml="http://namespaces.zope.org/zcml">

    <!-- Declare that plone.formwidget.captcha provides a Captcha field that
         can be used by plone.app.discussion to add a Captcha field to comment
         forms. -->
    <meta:provides feature="plone.app.discussion-captcha" />

</configure>

Note

Currently plone.formwidget.captcha and plone.formwidget.recaptcha claim to provide such a feature. If you want to write your own Captcha plugin, it has to provide this feature as well.

CaptchaExtender

The CaptchaExtender class extends the comment form with a Captcha field and widget. The CaptchaExtender currently uses either the CaptchaFieldWidget from plone.formwidget.captcha or the ReCaptchaFieldWidget from plone.formwidget.recaptcha. If you want to write your own Captcha solution, you have to override the update() method of the CaptchaExtender or write your own CaptchaExtender class.

class CaptchaExtender(extensible.FormExtender):
    """Extends the comment form with a Captcha. This Captcha extender is only
    registered when a plugin is installed that provides the
    "plone.app.discussion-captcha" feature.
    """
    adapts(Interface, IDefaultBrowserLayer, CommentForm) # context, request, form

    fields = Fields(ICaptcha)

    def __init__(self, context, request, form):
        self.context = context
        self.request = request
        self.form = form

        registry = queryUtility(IRegistry)
        settings = registry.forInterface(IDiscussionSettings, check=False)
        self.captcha = settings.captcha
        portal_membership = getToolByName(self.context, 'portal_membership')
        self.isAnon = portal_membership.isAnonymousUser()

    def update(self):
        if self.captcha != 'disabled' and self.isAnon:
            # Add a captcha field if captcha is enabled in the registry
            self.add(ICaptcha, prefix="")
            if self.captcha == 'captcha':
                from plone.formwidget.captcha import CaptchaFieldWidget
                self.form.fields['captcha'].widgetFactory = CaptchaFieldWidget
            elif self.captcha == 'recaptcha':
                from plone.formwidget.recaptcha import ReCaptchaFieldWidget
                self.form.fields['captcha'].widgetFactory = ReCaptchaFieldWidget
            elif self.captcha == 'norobots':
                from collective.z3cform.norobots import NorobotsFieldWidget
                self.form.fields['captcha'].widgetFactory = NorobotsFieldWidget
            else:
                self.form.fields['captcha'].mode = interfaces.HIDDEN_MODE

CaptchaValidator

The CaptchaValidator class provides custom versions of the plone.formwidget.captcha and the plone.formwidget.recaptcha validators. It does this, because we want to be able to have more than one Captcha solution installed in one Plone instance. We also want to be able to easily switch between different Captcha implementations inside a single Plone instance.

Therefore we have to check which Captcha solution is enabled in the discussion control panel and use only the selected Captcha validator. It is not enough to check if a Captcha plugin is just installed, because there could be more than one.

We do two checks. First we check for a suitable Captcha solution (check for the plone.app.discussion-captcha feature, see notes above). Second, we check which Captcha solution is enabled in the discussion control panel and apply the corresponding field validator.

The plone.app.discussion captcha validator always checks for a view with the name of the captcha plugin. For instance, if plone.formwidget.captcha is enabled it checks for a “captcha” view.

class CaptchaValidator(validator.SimpleFieldValidator):
    implements(IValidator)
    adapts(Interface, IDiscussionLayer, Interface, IField, Interface)
    #       Object, Request, Form, Field, Widget,
    # We adapt the CaptchaValidator class to all form fields (IField)

    def validate(self, value):
        super(CaptchaValidator, self).validate(value)

        registry = queryUtility(IRegistry)
        settings = registry.forInterface(IDiscussionSettings, check=False)

        if settings.captcha in ('captcha', 'recaptcha', 'norobots'):
            captcha = getMultiAdapter((aq_inner(self.context), self.request),
                                      name=settings.captcha)
            if not captcha.verify(input=value):
                if settings.captcha == 'norobots':
                    raise WrongNorobotsAnswer
                else:
                    raise WrongCaptchaCode
            else:
                return True


# Register Captcha validator for the Captcha field in the ICaptcha Form

Table Of Contents

Previous topic

Permissions and Workflows

Next topic

Email Notification

This Page