Index: quintagroup.formlib.captcha/trunk/quintagroup/formlib/captcha/__init__.py
===================================================================
--- quintagroup.formlib.captcha/trunk/quintagroup/formlib/captcha/__init__.py	(revision 1959)
+++ quintagroup.formlib.captcha/trunk/quintagroup/formlib/captcha/__init__.py	(revision 2210)
@@ -1,1 +1,2 @@
-#
+from widget import CaptchaWidget
+from field import Captcha
Index: quintagroup.formlib.captcha/trunk/quintagroup/formlib/captcha/captcha.txt
===================================================================
--- quintagroup.formlib.captcha/trunk/quintagroup/formlib/captcha/captcha.txt	(revision 2210)
+++ quintagroup.formlib.captcha/trunk/quintagroup/formlib/captcha/captcha.txt	(revision 2210)
@@ -0,0 +1,17 @@
+--------------------------
+Captchas in a formlib form
+--------------------------
+
+Using quintagroup.formlib.captcha in a formlib form is simple. Just add a
+Captcha field to your schema, and away you go:
+
+  >>> from zope.interface import Interface
+  >>> from quintagroup.formlib.captcha.form import Captcha
+  >>> class CaptchaSchema(Interface):
+  ...     captcha = Captcha(
+  ...         title=_(u'Type the code'),
+  ...         description=_(u'Type the code from the picture shown below.'))
+
+and formlib will take care of the rest. The widget associated with this field 
+will render the captcha and verify the use input automatically.
+
Index: quintagroup.formlib.captcha/trunk/quintagroup/formlib/captcha/configure.zcml
===================================================================
--- quintagroup.formlib.captcha/trunk/quintagroup/formlib/captcha/configure.zcml	(revision 1959)
+++ quintagroup.formlib.captcha/trunk/quintagroup/formlib/captcha/configure.zcml	(revision 2210)
@@ -1,10 +1,16 @@
 <configure
     xmlns="http://namespaces.zope.org/zope"
-    xmlns:zcml="http://namespaces.zope.org/zcml"
     i18n_domain="quintagroup.formlib.captcha">
 
-    <include package=".form"
-             zcml:condition="installed zope.app.form"/>
+  <include package="quintagroup.captcha.core" />
+  <include package="zope.formlib" />
 
+  <view
+      type="zope.publisher.interfaces.browser.IBrowserRequest"
+      for=".interfaces.ICaptcha"
+      provides="zope.app.form.interfaces.IInputWidget"
+      factory=".widget.CaptchaWidget"
+      permission="zope.Public"
+      />
 
 </configure>
Index: quintagroup.formlib.captcha/trunk/quintagroup/formlib/captcha/field.py
===================================================================
--- quintagroup.formlib.captcha/trunk/quintagroup/formlib/captcha/field.py	(revision 2210)
+++ quintagroup.formlib.captcha/trunk/quintagroup/formlib/captcha/field.py	(revision 2210)
@@ -0,0 +1,6 @@
+from zope.interface import implements
+from zope.schema import ASCIILine
+from interfaces import ICaptcha
+
+class Captcha(ASCIILine):
+    implements(ICaptcha)
Index: quintagroup.formlib.captcha/trunk/quintagroup/formlib/captcha/interfaces.py
===================================================================
--- quintagroup.formlib.captcha/trunk/quintagroup/formlib/captcha/interfaces.py	(revision 2210)
+++ quintagroup.formlib.captcha/trunk/quintagroup/formlib/captcha/interfaces.py	(revision 2210)
@@ -0,0 +1,4 @@
+from zope.schema.interfaces import IASCIILine
+
+class ICaptcha(IASCIILine):
+    u"""A field for captcha validation"""
Index: quintagroup.formlib.captcha/trunk/quintagroup/formlib/captcha/tests.py
===================================================================
--- quintagroup.formlib.captcha/trunk/quintagroup/formlib/captcha/tests.py	(revision 2210)
+++ quintagroup.formlib.captcha/trunk/quintagroup/formlib/captcha/tests.py	(revision 2210)
@@ -0,0 +1,137 @@
+import re
+import unittest
+
+from zope.schema.interfaces import IField
+from zope.component import queryMultiAdapter
+from zope.publisher.browser import TestRequest
+from zope.app.form.interfaces import IInputWidget
+from zope.app.form.interfaces import ConversionError
+
+from Products.Five import zcml
+from Products.Five import fiveconfigure
+from Testing import ZopeTestCase as ztc
+from Products.PloneTestCase.layer import onsetup
+from Products.PloneTestCase import PloneTestCase as ptc
+
+from quintagroup.captcha.core.utils import *
+from quintagroup.captcha.core.tests.base import testPatch
+from quintagroup.captcha.core.tests.testWidget import addTestLayer
+
+from quintagroup.formlib.captcha import Captcha
+from quintagroup.formlib.captcha import CaptchaWidget
+
+@onsetup
+def setup_product():
+    fiveconfigure.debug_mode = True
+    import quintagroup.captcha.core
+    import quintagroup.formlib.captcha
+    zcml.load_config('configure.zcml', quintagroup.formlib.captcha)
+    fiveconfigure.debug_mode = False
+    ztc.installPackage('quintagroup.captcha.core')
+
+setup_product()
+ptc.setupPloneSite(extension_profiles=['quintagroup.captcha.core:default',])
+
+
+class TestRegistrations(ptc.PloneTestCase):
+
+    def testCaptchaFieldInterface(self):
+        self.assertEqual(IField.implementedBy(Captcha), True)
+
+    def testCaptchaWidgetInterface(self):
+        self.assertEqual(IInputWidget.implementedBy(CaptchaWidget), True)
+
+    def testWidgetRegistration(self):
+        cfield = Captcha()
+        request = TestRequest()
+        cwidget = queryMultiAdapter((cfield, request), IInputWidget)
+        self.assertNotEqual(cwidget, None)
+
+
+class TestCaptchaWidgetHTML(ptc.PloneTestCase):
+    def afterSetUp(self):
+        # app context
+        self.loginAsPortalOwner()
+        # get html output from widget
+        field = Captcha()
+        bound = field.bind(self.portal)
+        widget = CaptchaWidget(bound, self.app.REQUEST)
+        widget.setPrefix('')
+        self.html = widget()
+
+    def testHidden(self):
+        HIDDENTAG = '<input\s+[^>]*(?:' \
+            '(?:type="hidden"\s*)|' \
+            '(?:name="hashkey"\s*)|' \
+            '(?:value="(?P<value>[0-9a-fA-F]+)"\s*)' \
+            '){3}/>'
+        hidden = re.search(HIDDENTAG, self.html)
+        self.assertTrue(hidden and hidden.group('value'))
+
+    def testImg(self):
+        IMAGETAG = '<img\s+[^>]*src=\"' \
+            '(?P<src>[^\"]*/getCaptchaImage/[0-9a-fA-F]+)' \
+            '\"[^>]*>'
+        img = re.search(IMAGETAG, self.html)
+        self.assertTrue(img and img.group('src'))
+
+    def testTextField(self):
+        FIELDTAG = '<input\s+[^>]*type=\"text\"\s*[^>]*>'
+        self.assertEqual(re.search(FIELDTAG, self.html) is not None, True)
+        
+
+class TestCaptchaWidgetToField(ptc.PloneTestCase):
+
+    def afterSetUp(self):
+        # prepare context
+        self.loginAsPortalOwner()
+        testPatch()
+        addTestLayer(self)
+        self.captcha_key = self.portal.captcha_key
+        # prepare widget
+        field = Captcha()
+        bound = field.bind(self.portal)
+        self.request = self.app.REQUEST
+        self.widget = CaptchaWidget(bound, self.request)
+        self.widget.setPrefix('')
+        # prepare captcha data
+        self.hashkey = self.portal.getCaptcha()
+        self.request.form['hashkey'] = self.hashkey
+
+    def testSubmitRightCaptcha(self):
+        decrypted = decrypt(self.captcha_key, self.hashkey)
+        key = getWord(int(parseKey(decrypted)['key'])-1 )
+        try:
+            res = self.widget._toFieldValue(key)
+        except ConversionError, e:
+            self.fail("Rised unexpected %s error on right captcha submit" % e.doc())
+        else:
+            self.assertEqual(res, key)
+        
+    def testSubmitWrongCaptcha(self):
+        try:
+            res = self.widget._toFieldValue("wrong key")
+        except ConversionError, e:
+            self.assertEqual(e.doc(), u'Please re-enter validation code.')
+        else:
+            self.fail("No ConversionError rised on wrong captcha key submit")
+
+    def testSubmitRightCaptchaTwice(self):
+        decrypted = decrypt(self.captcha_key, self.hashkey)
+        key = getWord(int(parseKey(decrypted)['key'])-1 )
+        self.widget._toFieldValue(key)
+        try:
+            res = self.widget._toFieldValue(key)
+        except ConversionError, e:
+            self.assertEqual(e.doc(), u'Please re-enter validation code.')
+        else:
+            self.fail("No ConversionError rised on right captcha key " \
+                      "submitting twice")
+
+
+def test_suite():
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.makeSuite(TestRegistrations))
+    suite.addTest(unittest.makeSuite(TestCaptchaWidgetHTML))
+    suite.addTest(unittest.makeSuite(TestCaptchaWidgetToField))
+    return suite
Index: quintagroup.formlib.captcha/trunk/quintagroup/formlib/captcha/widget.py
===================================================================
--- quintagroup.formlib.captcha/trunk/quintagroup/formlib/captcha/widget.py	(revision 2210)
+++ quintagroup.formlib.captcha/trunk/quintagroup/formlib/captcha/widget.py	(revision 2210)
@@ -0,0 +1,83 @@
+from DateTime import DateTime
+
+from zope.component import getMultiAdapter
+from zope.app.form.browser import ASCIIWidget
+from zope.app.form.interfaces import ConversionError
+from zope.app.form.browser.textwidgets import renderElement
+from zope.i18n import MessageFactory
+
+from Acquisition import aq_inner
+
+from Products.CMFCore.utils import getToolByName
+
+from quintagroup.captcha.core.utils import decrypt, parseKey, encrypt1, getWord
+
+_ = MessageFactory('quintagroup.formlib.captcha')
+
+
+class CaptchaWidget(ASCIIWidget):
+    def __call__(self):
+        context = self.context.context
+
+        kwargs = {'type': self.type,
+                  'name': self.name,
+                  'id': self.name,
+                  'cssClass': self.cssClass,
+                  'style': self.style,
+                  'size': self.displayWidth,
+                  'extra': self.extra}
+
+        portal_url = getToolByName(context, 'portal_url')()
+        key = context.getCaptcha()
+
+        if self._prefix:
+            prefix = '%s.' % self._prefix
+        else:
+            prefix = ''
+
+        return u"""<input type="hidden" value="%s" name="%shashkey" />
+                   %s
+                   <img src="%s/getCaptchaImage/%s" alt="Enter the word"/>""" % (key,
+                                                                                 prefix,
+                                                                                 renderElement(self.tag, **kwargs),
+                                                                                 portal_url,
+                                                                                 key)
+         
+    def _toFieldValue(self, input):
+        # Verify the user input against the captcha
+        context = self.context.context
+        request = context.REQUEST
+        
+        # get captcha type (static or dynamic)
+        captcha_type = context.getCaptchaType()
+        
+        # validate captcha input
+        if input and captcha_type in ['static', 'dynamic']:
+            # make up form prefix
+            if self._prefix:
+                prefix = '%s.' % self._prefix
+            else:
+                prefix = ''
+            
+            hashkey = request.get('%shashkey' % prefix, '')
+            decrypted_key = decrypt(context.captcha_key, hashkey)
+            parsed_key = parseKey(decrypted_key)
+            
+            index = parsed_key['key']
+            date = parsed_key['date']
+            
+            if captcha_type == 'static':
+                img = getattr(context, '%s.jpg' % index)
+                solution = img.title
+                enc = encrypt1(input)
+            else:
+                enc = input
+                solution = getWord(int(index))
+            
+            captcha_tool = getToolByName(context, 'portal_captchas')
+            if (enc != solution) or (captcha_tool.has_key(decrypted_key)) or (DateTime().timeTime() - float(date) > 3600):
+                raise ConversionError(_(u'Please re-enter validation code.'))
+            else:
+                captcha_tool.addExpiredKey(decrypted_key)
+        
+        return super(CaptchaWidget, self)._toFieldValue(input)
