Index: main/waeup.sirp/trunk/src/waeup/sirp/browser/captcha.py
===================================================================
--- main/waeup.sirp/trunk/src/waeup/sirp/browser/captcha.py	(revision 7281)
+++ main/waeup.sirp/trunk/src/waeup/sirp/browser/captcha.py	(revision 7281)
@@ -0,0 +1,232 @@
+##
+## captcha.py
+## Login : <uli@pu.smp.net>
+## Started on  Sun Dec  4 17:18:01 2011 Uli Fouquet
+## $Id$
+## 
+## Copyright (C) 2011 Uli Fouquet
+## This program is free software; you can redistribute it and/or modify
+## it under the terms of the GNU General Public License as published by
+## the Free Software Foundation; either version 2 of the License, or
+## (at your option) any later version.
+## 
+## This program is distributed in the hope that it will be useful,
+## but WITHOUT ANY WARRANTY; without even the implied warranty of
+## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+## GNU General Public License for more details.
+## 
+## You should have received a copy of the GNU General Public License
+## along with this program; if not, write to the Free Software
+## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+##
+"""Components to add captcha functionality in views/pages.
+
+This is currently a playground, stuff still to be worked out more properly.
+"""
+import grok
+import urllib
+import urllib2
+from zope import schema
+from zope.interface import Interface
+from zope.publisher.interfaces.http import IHTTPRequest
+from waeup.sirp.browser import WAeUPPage as SIRPPage
+from waeup.sirp.interfaces import IUniversity
+
+
+grok.templatedir('templates')
+
+class ICaptchaRequest(Interface):
+    """A set of data required to verify captcha solutions.
+
+    To solve a captcha we need at least a solution. Many types of
+    captchas might also need a challenge to compare whether the
+    solution is correct.
+    """
+    solution = schema.TextLine(
+        title = u'Solution string a user entered',
+        default = None,
+        )
+    challenge = schema.TextLine(
+        title = u'The challenge the solution might match',
+        default = None,
+        )
+
+class ICaptchaResponse(Interface):
+    """A formalized response for captcha solutions.
+    """
+    is_valid = schema.Bool(
+        title = u'Indicates validity of entered captcha data.',
+        default = False,
+        )
+    error_code = schema.TextLine(
+        title = u'Error when trying to validate entered captcha data.',
+        description = u'Error codes are not expected to be readable strings.',
+        default = None,
+        )
+
+class ICaptcha(Interface):
+
+    def verify(request_data):
+        """Verify data entered in a request.
+
+        Expects some ICaptchaRequest object and returns an
+        ICaptchaResponse indicating that the solution was correct or
+        not.
+
+        If the solution could not be verified (this might also happen
+        because of technical reasons), the response might contain an
+        error code.
+        """
+
+    def display(error_code=None):
+        """Returns a piece of HTML code that displays the captcha.
+
+        The generated HTML code might depend on any previous error
+        code as returned from :meth:`verify`.
+
+        The code is expected to appear inside a ``<form>``. It
+        therefore should not contain a ``<form>`` nor any submit
+        buttons.
+        """
+
+class ICaptchaConfig(Interface):
+    """Any type of captcha might need some configuration data.
+
+    By default we require no configuration data.
+    """
+
+#
+# Trivial default captcha
+#
+class CaptchaResponse(object):
+    grok.implements(ICaptchaResponse)
+    def __init__(self, is_valid, error_code=None):
+        self.is_valid = is_valid
+        self.error_code = error_code
+        return
+
+class CaptchaRequest(object):
+    grok.implements(ICaptchaRequest)
+    def __init__(self, solution=None, challenge=None):
+        self.solution = solution
+        self.challenge = challenge
+        return
+
+class NullCaptcha(grok.Adapter):
+    """A captcha that does not expect any input.
+
+    Meant as a placeholder for sites that do not want captchas at all.
+
+    NullCaptchas do not render any HTML/JavaScript and accept any
+    request when asked for verification.
+
+    They can be used in pages prepared for captchas where the site
+    maintainer decides not to use it at all.
+    """
+    grok.context(IHTTPRequest)
+    grok.implements(ICaptcha)
+    grok.name('No captcha')
+
+    def __init__(self, context=None):
+        self.context = None
+
+    def verify(self, captcha_request):
+        return CaptchaResponse(True, None)
+
+    def display(self, error_code=None):
+        return u''
+
+
+#
+# ReCaptcha
+#
+API_SSL_SERVER = "https://www.google.com/recaptcha/api"
+VERIFY_SERVER = "http://www.google.com"
+PUBLIC_KEY = "6Lc0y8oSAAAAAHwdojrqPtcKn7Rww5qGprb0rrSk"
+PRIVATE_KEY = "6Lc0y8oSAAAAAMHVbMrGWLLjw2pm8v2Uprwm9AbR"
+
+
+
+class RecaptchaResponse(object):
+    def __init__(self, is_valid, error_code=None):
+        self.is_valid = is_valid
+        self.error_code = error_code
+        return
+
+class CaptchaTestPage(SIRPPage):
+    grok.name('captcha')
+    grok.context(IUniversity)
+    grok.require('waeup.Public')
+    title = 'Captcha Test'
+    label = title
+
+    def update(self, recaptcha_challenge_field=None,
+               recaptcha_response_field=None):
+        self.error_code = None
+        print "CHAL: ", recaptcha_challenge_field
+        print "RESP: ", recaptcha_response_field
+        print self.request.form.items()
+        verify_result = 'NONE'
+        if recaptcha_challenge_field and recaptcha_response_field:
+            verify_result = self.verify(
+                recaptcha_challenge_field, recaptcha_response_field)
+        print "VERIFY: ", verify_result
+        if verify_result != 'NONE':
+            print "  VALID: ", verify_result.is_valid
+            print "  ERR: ", verify_result.error_code
+            self.error_code = verify_result.error_code
+        pass
+
+    def verify(self, challenge, solution, private_key=PRIVATE_KEY,
+               remote_ip='127.0.0.1'):
+        if not challenge or not solution:
+            return RecaptchaResponse(
+                is_valid=False, error_code='incorrect-captcha-sol')
+        params = urllib.urlencode(
+            {
+                'privatekey': private_key,
+                'remoteip': remote_ip,
+                'challenge': challenge,
+                'response': solution,
+                })
+        request = urllib2.Request(
+            url = "%s/recaptcha/api/verify" % VERIFY_SERVER,
+            data = params,
+            headers = {
+                "Content-type": "application/x-www-form-urlencoded",
+                "User-agent": "reCAPTCHA Python SIRP",
+                }
+            )
+        resp = urllib2.urlopen(request)
+        ret_vals = resp.read().splitlines()
+        resp.close()
+        ret_code, err_code = ret_vals
+
+        if ret_code == "true":
+            return RecaptchaResponse(is_valid=True)
+        return RecaptchaResponse(is_valid=False, error_code=ret_vals[1])
+
+    def render(self):
+        error_param = ''
+        if self.error_code:
+            error_param = '&error=%s' % self.error_code
+        return """
+  <form method="POST" action="">
+    <script type="text/javascript"
+            src="%(ApiServer)s/challenge?k=%(PublicKey)s%(ErrorParam)s">
+    </script>
+
+    <noscript>
+      <iframe src="%(ApiServer)s/noscript?k=%(PublicKey)s%(ErrorParam)s"
+              height="300" width="500" frameborder="0"></iframe><br />
+      <textarea name="recaptcha_challenge_field" rows="3" cols="40"></textarea>
+      <input type='hidden' name='recaptcha_response_field'
+             value='manual_challenge' />
+    </noscript>
+    <input type="submit" name="SUBMIT" />
+  </form>
+""" % {
+        'ApiServer' : API_SSL_SERVER,
+        'PublicKey' : PUBLIC_KEY,
+        'ErrorParam' : error_param,
+        }
