## $Id: captcha.py 15012 2018-05-19 21:47:54Z uli $
##
## 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 json
import urllib
import urllib2
from random import SystemRandom as random
from zope import schema
from zope.component import getUtilitiesFor, getUtility, queryUtility
from zope.interface import Interface
from zope.publisher.interfaces.http import IHTTPRequest
from waeup.kofa.browser.layout import KofaPage
from waeup.kofa.browser.interfaces import (
ICaptchaRequest, ICaptchaResponse, ICaptcha, ICaptchaConfig,
ICaptchaManager)
from waeup.kofa.interfaces import IUniversity
#
# Global captcha manager
#
[docs]class CaptchaManager(grok.GlobalUtility):
grok.implements(ICaptchaManager)
[docs] def getAvailCaptchas(self):
"""Get all available captchas registered as utils for ICaptcha.
The default captcha (as it most probably is a copy of another
registered captcha) is left out of the result.
Result will be a dict with registration names as keys and the
specific captcha instances as values.
"""
result = getUtilitiesFor(ICaptcha)
return dict([(name,inst) for name,inst in result
if name != u''])
[docs] def getCaptcha(self):
"""Get captcha chosen to be used.
Sites can activate a specific captcha by setting
``site['configuration'].captcha``. The attribute should be a
string under which the specific captcha is registered.
If this attribute is not set or we are
not 'in a site', the default captcha is returned.
"""
site = grok.getSite()
name = ''
if site is None:
return getUtility(ICaptcha)
name = getattr(site.get('configuration', {}), 'captcha', u'')
return queryUtility(ICaptcha, name=name,
default=getUtility(ICaptcha))
##
## Trivial default captcha
##
[docs]class CaptchaResponse(object):
grok.implements(ICaptchaResponse)
[docs] def __init__(self, is_valid, error_code=None):
self.is_valid = is_valid
self.error_code = error_code
return
[docs]class CaptchaRequest(object):
grok.implements(ICaptchaRequest)
[docs] def __init__(self, solution=None, challenge=None):
self.solution = solution
self.challenge = challenge
return
[docs]class NullCaptcha(object):
"""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.implements(ICaptcha)
[docs] def verify(self, request):
return CaptchaResponse(True, None)
[docs] def display(self, error_code=None):
return u''
# This captcha is registered twice: one time as a 'no captcha' captcha
# and then also as the default captcha (with empty name)
grok.global_utility(NullCaptcha, name=u'No captcha')
grok.global_utility(NullCaptcha, name=u'')
##
## TestCaptcha
##
[docs]class StaticCaptcha(object):
"""The StaticCaptcha always has the same solution: 'the-solution'.
It is of no use for real world but for tests. In tests we cannot
easily solve really strong captchas. But we can use a captcha that
works like a real one but is easy to solve even for machines.
The HTML form piece generated is even prefilled with the correct
solution. So in tests it is not necessary to always 'type' the
solution string in the correct field.
You can, however, fill in a wrong solution and it will be detected
as such.
"""
grok.implements(ICaptcha)
#: name of solution field in HTTP request
sol_field = 'solution'
#: name of challenge field in HTTP request
chal_field = 'challenge'
[docs] def verify(self, request):
"""Verify that a solution sent equals the challenge.
"""
form = getattr(request, 'form', {})
solution=form.get(self.sol_field, None)
challenge=form.get(self.chal_field, None)
if solution == challenge and solution:
return CaptchaResponse(is_valid=True)
return CaptchaResponse(is_valid=False)
[docs] def display(self, error_code=None):
"""Display challenge and input field for solution as HTML.
"""
html = (
u'<input type="hidden" name="challenge"'
u' value="the-solution" /><br />'
u'Type: %s<br />'
u'<input type="text" name="solution" value="the-solution"'
u' /><br />')
return html
grok.global_utility(StaticCaptcha, name=u'Testing captcha')
##
## ReCaptcha (v2)
##
[docs]class ReCaptcha(StaticCaptcha):
"""ReCaptcha - strong captchas with images, sound, etc.
This is the Kofa implementation to support captchas as provided by
http://www.google.com/recaptcha (v2).
ReCaptcha is widely used and adopted in web applications. See the
above web page to learn more about recaptcha.
Basically, it generates a captcha box in a page loaded by a
client. The client can then enter a solution for a challenge
picture (or audio file) and send the solution back to our server
along with the challenge string generated locally on the client.
This component then verifies the entered solution deploying the
private key set by asking a verification server. The result (valid
or invalid solution) is then returned to any calling component.
As any captcha-component, :class:`ReCaptcha` can be used by any
other component that wants to display/verify captchas.
To incorporate captcha usage in a view, page, or viewlet, the
following steps have to be performed:
* get the currently site-wide selected captcha type by doing::
mycaptcha = getUtility(ICaptchaManager).getCaptcha()
* if you want a specific captcha type (like ReCaptcha)::
mycaptcha = getUtility(ICaptcha, name='ReCaptcha')
Now, as you have a captcha, you can verify sent data by doing::
result = mycaptcha.verify(self.request)
where ``self.request`` should be the sent HTTP request and
``result`` will be an instance of class:``CaptchaResponse``. The
response will contain an error code (``result.error_code``) that
might be ``None`` but can (and should) be passed to the
:meth:``display`` method to display error messages in the captcha
box. The error code is most probably not a human readable string
but some code you shouldn't rely upon.
All this could be done in the ``update()`` method of a view, page,
or viewlet.
To render the needed HTML code, you can deploy the
:meth:`display`` method of ``mycaptcha``.
This captcha is available at runtime as a global utility named
``'ReCaptcha'``.
Shortcomings:
- no support for selecting theme
- no support for selecting box layout (normal, compact)
- no support for non-javascript users
- Google will get the data of our clients
- Google will track all activity of our clients
"""
grok.implements(ICaptcha)
#: name of response token field in HTTP request
token_field = 'g-recaptcha-response'
# Do not use the following keys in productive environments! As
# they are both made publicly available, they are not secure any
# more! Use them for testing and evaluating only!
PUBLIC_KEY = "6Lc0y8oSAAAAAHwdojrqPtcKn7Rww5qGprb0rrSk"
PRIVATE_KEY = "6Lc0y8oSAAAAAMHVbMrGWLLjw2pm8v2Uprwm9AbR"
VERIFY_URL = 'https://www.google.com/recaptcha/api/siteverify'
[docs] def verify(self, request):
"""Grab challenge/solution from HTTP request and verify it.
Verification happens against recaptcha remote API servers. It
only happens, when a repsonse token was sent with the
request.
Returns a :class:`CaptchaResponse` indicating that the
verification failed or succeeded.
"""
form = getattr(request, 'form', {})
token = form.get(self.token_field, None)
if not token:
# on first-time display, we won't get a token
return CaptchaResponse(is_valid=False)
params = urllib.urlencode(
{
'secret': self.PRIVATE_KEY,
'response': token,
'remoteip': '127.0.0.1',
})
request = urllib2.Request(
url = self.VERIFY_URL,
data = params,
headers = {
"Content-type": "application/x-www-form-urlencoded",
"User-agent": "reCAPTCHA Python Kofa",
}
)
conn = urllib2.urlopen(request)
ret_vals = json.loads(conn.read())
conn.close()
if ret_vals.get('success', False) is True:
return CaptchaResponse(is_valid=True)
return CaptchaResponse(
is_valid=False, error_code="%s" % ret_vals['error-codes'])
[docs] def display(self, error_code=None):
"""Display captcha widget snippet.
Returns the HTML code to be placed inside an existing ``<form>``
of your page. You can add other fields and should add a submit
button to send the form.
The ``error_code`` can be taken from a previously fetched
:class:`CaptchaResponse` instance (as returned by
:meth:`verify`). If it is not ``None``, it might be
displayed inside the generated captcha box (in human readable
form).
"""
error_param = ''
if error_code:
error_param = '&error=%s' % error_code
html = (
u'<script type="text/javascript" '
u'src="%(ApiServer)s" async defer>'
u'</script>'
u'<div class="g-recaptcha" data-sitekey="%(SiteKey)s"></div>' % {
'ApiServer': "https://www.google.com/recaptcha/api.js",
'SiteKey': self.PUBLIC_KEY,
}
)
return html
grok.global_utility(ReCaptcha, name=u'ReCaptcha')
[docs]class CaptchaTestPage(KofaPage):
# A test page to see a captcha in action
grok.name('captcha')
grok.context(IUniversity)
grok.require('waeup.Public')
title = 'Captcha Test'
label = title
[docs] def update(self, recaptcha_challenge_field=None,
recaptcha_response_field=None):
self.captcha = getUtility(ICaptcha, name='ReCaptcha')
result = self.captcha.verify(self.request)
self.captcha_error = result.error_code
print "VERIFY: ", result.is_valid, result.error_code
return
[docs] def render(self):
return """
<form method="POST" action="">
%s
<input type="submit" name="SUBMIT" />
</form>
""" % (self.captcha.display(self.captcha_error),)