source: main/waeup.kofa/branches/henrik-regista/src/waeup/ikoba/browser/captcha.py @ 11952

Last change on this file since 11952 was 11952, checked in by Henrik Bettermann, 10 years ago

More renaming: University -> Institution, Student -> Customer
Change portal title.

  • Property svn:keywords set to Id
File size: 11.9 KB
Line 
1## $Id: captcha.py 11952 2014-11-13 16:03:08Z henrik $
2##
3## Copyright (C) 2011 Uli Fouquet
4## This program is free software; you can redistribute it and/or modify
5## it under the terms of the GNU General Public License as published by
6## the Free Software Foundation; either version 2 of the License, or
7## (at your option) any later version.
8##
9## This program is distributed in the hope that it will be useful,
10## but WITHOUT ANY WARRANTY; without even the implied warranty of
11## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12## GNU General Public License for more details.
13##
14## You should have received a copy of the GNU General Public License
15## along with this program; if not, write to the Free Software
16## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17##
18"""Components to add captcha functionality in views/pages.
19
20This is currently a playground, stuff still to be worked out more properly.
21"""
22import grok
23import urllib
24import urllib2
25from random import SystemRandom as random
26from zope import schema
27from zope.component import getUtilitiesFor, getUtility, queryUtility
28from zope.interface import Interface
29from zope.publisher.interfaces.http import IHTTPRequest
30from waeup.ikoba.browser.layout import IkobaPage
31from waeup.ikoba.browser.interfaces import (
32    ICaptchaRequest, ICaptchaResponse, ICaptcha, ICaptchaConfig,
33    ICaptchaManager)
34from waeup.ikoba.interfaces import IInstitution
35
36#
37# Global captcha manager
38#
39class CaptchaManager(grok.GlobalUtility):
40
41    grok.implements(ICaptchaManager)
42
43    def getAvailCaptchas(self):
44        """Get all available captchas registered as utils for ICaptcha.
45
46        The default captcha (as it most probably is a copy of another
47        registered captcha) is left out of the result.
48
49        Result will be a dict with registration names as keys and the
50        specific captcha instances as values.
51        """
52        result = getUtilitiesFor(ICaptcha)
53        return dict([(name,inst) for name,inst in result
54                       if name != u''])
55
56    def getCaptcha(self):
57        """Get captcha chosen to be used.
58
59        Sites can activate a specific captcha by setting
60        ``site['configuration'].captcha``. The attribute should be a
61        string under which the specific captcha is registered.
62
63        If this attribute is not set or we are
64        not 'in a site', the default captcha is returned.
65        """
66        site = grok.getSite()
67        name = ''
68        if site is None:
69            return getUtility(ICaptcha)
70        name = getattr(site.get('configuration', {}), 'captcha', u'')
71        return queryUtility(ICaptcha, name=name,
72                            default=getUtility(ICaptcha))
73
74##
75## Trivial default captcha
76##
77class CaptchaResponse(object):
78    grok.implements(ICaptchaResponse)
79    def __init__(self, is_valid, error_code=None):
80        self.is_valid = is_valid
81        self.error_code = error_code
82        return
83
84class CaptchaRequest(object):
85    grok.implements(ICaptchaRequest)
86    def __init__(self, solution=None, challenge=None):
87        self.solution = solution
88        self.challenge = challenge
89        return
90
91class NullCaptcha(object):
92    """A captcha that does not expect any input.
93
94    Meant as a placeholder for sites that do not want captchas at all.
95
96    NullCaptchas do not render any HTML/JavaScript and accept any
97    request when asked for verification.
98
99    They can be used in pages prepared for captchas where the site
100    maintainer decides not to use it at all.
101    """
102    grok.implements(ICaptcha)
103
104    def verify(self, request):
105        return CaptchaResponse(True, None)
106
107    def display(self, error_code=None):
108        return u''
109
110
111# This captcha is registered twice: one time as a 'no captcha' captcha
112# and then also as the default captcha (with empty name)
113grok.global_utility(NullCaptcha, name=u'No captcha')
114grok.global_utility(NullCaptcha, name=u'')
115
116##
117## TestCaptcha
118##
119class StaticCaptcha(object):
120    """The StaticCaptcha always has the same solution: 'the-solution'.
121
122    It is of no use for real world but for tests. In tests we cannot
123    easily solve really strong captchas. But we can use a captcha that
124    works like a real one but is easy to solve even for machines.
125
126    The HTML form piece generated is even prefilled with the correct
127    solution. So in tests it is not necessary to always 'type' the
128    solution string in the correct field.
129
130    You can, however, fill in a wrong solution and it will be detected
131    as such.
132    """
133    grok.implements(ICaptcha)
134
135    #: name of solution field in HTTP request
136    sol_field = 'solution'
137    #: name of challenge field in HTTP request
138    chal_field = 'challenge'
139
140    def verify(self, request):
141        """Verify that a solution sent equals the challenge.
142        """
143        form = getattr(request, 'form', {})
144        solution=form.get(self.sol_field, None)
145        challenge=form.get(self.chal_field, None)
146        if solution == challenge and solution:
147            return CaptchaResponse(is_valid=True)
148        return CaptchaResponse(is_valid=False)
149
150    def display(self, error_code=None):
151        """Display challenge and input field for solution as HTML.
152        """
153        html = (
154            u'<input type="hidden" name="challenge"'
155            u'       value="the-solution" /><br />'
156            u'Type: %s<br />'
157            u'<input type="text" name="solution" value="the-solution"'
158            u' /><br />')
159        return html
160
161grok.global_utility(StaticCaptcha, name=u'Testing captcha')
162
163
164##
165## ReCaptcha
166##
167API_SSL_SERVER = "https://www.google.com/recaptcha/api"
168VERIFY_SERVER = "https://www.google.com/recaptcha/api"
169
170class ReCaptcha(StaticCaptcha):
171    """ReCaptcha - strong captchas with images, sound, etc.
172
173    This is the Ikoba implementation to support captchas as provided by
174    http://www.google.com/recaptcha.
175
176    ReCaptcha is widely used and adopted in web applications. See the
177    above web page to learn more about recaptcha.
178
179    Basically, it generates a captcha box in a page loaded by a
180    client. The client can then enter a solution for a challenge
181    picture (or audio file) and send the solution back to our server
182    along with the challenge string generated locally on the client.
183
184    This component then verifies the entered solution deploying the
185    private key set by asking a verification server. The result (valid
186    or invalid solution) is then returned to any calling component.
187
188    As any captcha-component, :class:`ReCaptcha` can be used by any
189    other component that wants to display/verify captchas.
190
191    To incorporate captcha usage in a view, page, or viewlet, the
192    following steps have to be performed:
193
194    * get the currently site-wide selected captcha type by doing::
195
196        mycaptcha = getUtility(ICaptchaManager).getCaptcha()
197
198    * if you want a specific captcha type (like ReCaptcha)::
199
200        mycaptcha = getUtility(ICaptcha, name='ReCaptcha')
201
202    Now, as you have a captcha, you can verify sent data by doing::
203
204        result = mycaptcha.verify(self.request)
205
206    where ``self.request`` should be the sent HTTP request and
207    ``result`` will be an instance of class:``CaptchaResponse``. The
208    response will contain an error code (``result.error_code``) that
209    might be ``None`` but can (and should) be passed to the
210    :meth:``display`` method to display error messages in the captcha
211    box. The error code is most probably not a human readable string
212    but some code you shouldn't rely upon.
213
214    All this could be done in the ``update()`` method of a view, page,
215    or viewlet.
216
217    To render the needed HTML code, you can deploy the
218    :meth:`display`` method of ``mycaptcha``.
219
220    This captcha is available at runtime as a global utility named
221    ``'ReCaptcha'``.
222    """
223
224    grok.implements(ICaptcha)
225
226    #: name of solution field in HTTP request
227    sol_field = 'recaptcha_response_field'
228    #: name of challenge field in HTTP request
229    chal_field = 'recaptcha_challenge_field'
230
231    # Do not use the following keys in productive environments!  As
232    # they are both made publicly available, they are not secure any
233    # more!  Use them for testing and evaluating only!
234    PUBLIC_KEY = "6Lc0y8oSAAAAAHwdojrqPtcKn7Rww5qGprb0rrSk"
235    PRIVATE_KEY = "6Lc0y8oSAAAAAMHVbMrGWLLjw2pm8v2Uprwm9AbR"
236
237    def verify(self, request):
238        """Grab challenge/solution from HTTP request and verify it.
239
240        Verification happens against recaptcha remote API servers. It
241        only happens, when really a solution was sent with the
242        request.
243
244        Returns a :class:`CaptchaResponse` indicating that the
245        verification failed or succeeded.
246        """
247        form = getattr(request, 'form', {})
248        solution=form.get(self.sol_field, None)
249        challenge=form.get(self.chal_field, None)
250        if not challenge or not solution:
251            # Might be first-time display of the captcha: not a valid
252            # solution but no error code to prevent any error
253            # messages. Skip further verification.
254            return CaptchaResponse(
255                is_valid=False)
256        params = urllib.urlencode(
257            {
258                'privatekey': self.PRIVATE_KEY,
259                'remoteip': '127.0.0.1',
260                'challenge': challenge,
261                'response': solution,
262                })
263        request = urllib2.Request(
264            url = "%s/verify" % VERIFY_SERVER,
265            data = params,
266            headers = {
267                "Content-type": "application/x-www-form-urlencoded",
268                "User-agent": "reCAPTCHA Python Ikoba",
269                }
270            )
271        resp = urllib2.urlopen(request)
272        ret_vals = resp.read().splitlines()
273        resp.close()
274        ret_code, err_code = ret_vals
275
276        if ret_code == "true":
277            return CaptchaResponse(is_valid=True)
278        return CaptchaResponse(is_valid=False, error_code=ret_vals[1])
279
280    def display(self, error_code=None):
281        """Display challenge and input field for solution as HTML.
282
283        Returns the HTML code to be placed inside an existing ``<form>``
284        of your page. You can add other fields and should add a submit
285        button to send the form.
286
287        The ``error_code`` can be taken from a previously fetched
288        :class:``CaptchaResponse`` instance (as returned by
289        :meth:``verify``). If it is not ``None``, it might be
290        displayed inside the generated captcha box (in human readable
291        form).
292        """
293        error_param = ''
294        if error_code:
295            error_param = '&error=%s' % error_code
296
297        html = (
298            u'<script type="text/javascript" '
299            u'src="%(ApiServer)s/challenge?k=%(PublicKey)s%(ErrorParam)s">'
300            u'</script>'
301            u''
302            u'<noscript>'
303            u'<iframe'
304            u'    src="%(ApiServer)s/noscript?k=%(PublicKey)s%(ErrorParam)s"'
305            u'    height="300" width="500" frameborder="0"></iframe><br />'
306            u'<textarea name="recaptcha_challenge_field"'
307            u'          rows="3" cols="40"></textarea>'
308            u'<input type="hidden" name="recaptcha_response_field"'
309            u'       value="manual_challenge" />'
310            u'</noscript>' % {
311                'ApiServer' : API_SSL_SERVER,
312                'PublicKey' : self.PUBLIC_KEY,
313                'ErrorParam' : error_param,
314                }
315            )
316        return html
317
318grok.global_utility(ReCaptcha, name=u'ReCaptcha')
319
320
321class CaptchaTestPage(IkobaPage):
322    # A test page to see a captcha in action
323    grok.name('captcha')
324    grok.context(IInstitution)
325    grok.require('waeup.Public')
326    title = 'Captcha Test'
327    label = title
328
329    def update(self, recaptcha_challenge_field=None,
330               recaptcha_response_field=None):
331        self.captcha = getUtility(ICaptcha, name='ReCaptcha')
332        result = self.captcha.verify(self.request)
333        self.captcha_error = result.error_code
334        print "VERIFY: ", result.is_valid, result.error_code
335        return
336
337    def render(self):
338        return """
339  <form method="POST" action="">
340    %s
341    <input type="submit" name="SUBMIT" />
342  </form>
343  """ % (self.captcha.display(self.captcha_error),)
Note: See TracBrowser for help on using the repository browser.