Skip to content

Commit

Permalink
Use reCaptcha api v2
Browse files Browse the repository at this point in the history
No need to change existing code, this just works.
  • Loading branch information
git-commit committed Dec 5, 2014
1 parent 990ff65 commit 92c6e39
Show file tree
Hide file tree
Showing 3 changed files with 50 additions and 82 deletions.
7 changes: 7 additions & 0 deletions flask_wtf/_compat.py
Expand Up @@ -12,3 +12,10 @@ def to_bytes(text):
if isinstance(text, text_type):
text = text.encode('utf-8')
return text


def from_bytes(bytes, encoding='utf-8'):
"""Decodes bytes to text if needed."""
if not isinstance(bytes, string_types):
bytes = bytes.decode(encoding)
return bytes
46 changes: 19 additions & 27 deletions flask_wtf/recaptcha/validators.py
Expand Up @@ -7,27 +7,23 @@
from flask import request, current_app
from wtforms import ValidationError
from werkzeug import url_encode
from .._compat import to_bytes
from .._compat import to_bytes, from_bytes
import json

RECAPTCHA_VERIFY_SERVER = 'https://www.google.com/recaptcha/api/verify'
RECAPTCHA_VERIFY_SERVER = 'https://www.google.com/recaptcha/api/siteverify'

__all__ = ["Recaptcha"]


class Recaptcha(object):

"""Validates a ReCaptcha."""

_error_codes = {
'invalid-site-public-key': 'The public key for reCAPTCHA is invalid',
'invalid-site-private-key': 'The private key for reCAPTCHA is invalid',
'invalid-referrer': (
'The public key for reCAPTCHA is not valid for '
'this domainin'
),
'verify-params-incorrect': (
'The parameters passed to reCAPTCHA '
'verification are incorrect'
)
'missing-input-secret': 'The secret parameter is missing.',
'invalid-input-secret': 'The secret parameter is invalid or malformed.',
'missing-input-response': 'The response parameter is missing.',
'invalid-input-response': 'The response parameter is invalid or malformed.',
}

def __init__(self, message=u'Invalid word. Please try again.'):
Expand All @@ -38,46 +34,42 @@ def __call__(self, form, field):
return True

if request.json:
challenge = request.json.get('recaptcha_challenge_field', '')
response = request.json.get('recaptcha_response_field', '')
response = request.json.get('g-recaptcha-response', '')
else:
challenge = request.form.get('recaptcha_challenge_field', '')
response = request.form.get('recaptcha_response_field', '')
response = request.form.get('g-recaptcha-response', '')
remote_ip = request.remote_addr

if not challenge or not response:
if not response:
raise ValidationError(field.gettext(self.message))

if not self._validate_recaptcha(challenge, response, remote_ip):
if not self._validate_recaptcha(response, remote_ip):
field.recaptcha_error = 'incorrect-captcha-sol'
raise ValidationError(field.gettext(self.message))

def _validate_recaptcha(self, challenge, response, remote_addr):
def _validate_recaptcha(self, response, remote_addr):
"""Performs the actual validation."""
try:
private_key = current_app.config['RECAPTCHA_PRIVATE_KEY']
except KeyError:
raise RuntimeError("No RECAPTCHA_PRIVATE_KEY config set")

data = url_encode({
'privatekey': private_key,
'secret': private_key,
'remoteip': remote_addr,
'challenge': challenge,
'response': response
})

response = http.urlopen(RECAPTCHA_VERIFY_SERVER, to_bytes(data))
http_response = http.urlopen(RECAPTCHA_VERIFY_SERVER, to_bytes(data))

if response.code != 200:
if http_response.code != 200:
return False

rv = [l.strip() for l in response.readlines()]
json_resp = json.loads(from_bytes(http_response.read()))

if rv and rv[0] == to_bytes('true'):
if json_resp["success"]:
return True

if len(rv) > 1:
error = rv[1]
for error in json_resp["error-codes"]:
if error in self._error_codes:
raise RuntimeError(self._error_codes[error])

Expand Down
79 changes: 24 additions & 55 deletions flask_wtf/recaptcha/widgets.py
@@ -1,31 +1,31 @@
# -*- coding: utf-8 -*-

from flask import current_app, Markup
from werkzeug import url_encode
from flask import json
from .._compat import text_type
JSONEncoder = json.JSONEncoder

try:
from speaklater import _LazyString

class _JSONEncoder(JSONEncoder):
def default(self, o):
if isinstance(o, _LazyString):
return str(o)
return JSONEncoder.default(self, o)
except ImportError:
_JSONEncoder = JSONEncoder


RECAPTCHA_API_SERVER = '//www.google.com/recaptcha/api/'
RECAPTCHA_HTML = u'''
<script type="text/javascript">var RecaptchaOptions = %(options)s;</script>
<script type="text/javascript" src="%(script_url)s"></script>
<script src='https://www.google.com/recaptcha/api.js'></script>
<div class="g-recaptcha" data-sitekey="%(public_key)s"></div>
<noscript>
<iframe src="%(frame_url)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">
<div style="width: 302px; height: 352px;">
<div style="width: 302px; height: 352px; position: relative;">
<div style="width: 302px; height: 352px; position: absolute;">
<iframe src="https://www.google.com/recaptcha/api/fallback?k=%(public_key)s"
frameborder="0" scrolling="no"
style="width: 302px; height:352px; border-style: none;">
</iframe>
</div>
<div style="width: 250px; height: 80px; position: absolute; border-style: none;
bottom: 21px; left: 25px; margin: 0px; padding: 0px; right: 25px;">
<textarea id="g-recaptcha-response" name="g-recaptcha-response"
class="g-recaptcha-response"
style="width: 250px; height: 80px; border: 1px solid #c1c1c1;
margin: 0px; padding: 0px; resize: none;" value="">
</textarea>
</div>
</div>
</div>
</noscript>
'''

Expand All @@ -34,15 +34,11 @@ def default(self, o):

class RecaptchaWidget(object):

def recaptcha_html(self, query, options):
def recaptcha_html(self, public_key):
html = current_app.config.get('RECAPTCHA_HTML', RECAPTCHA_HTML)
server = current_app.config.get(
'RECAPTCHA_API_SERVER', RECAPTCHA_API_SERVER
)

return Markup(html % dict(
script_url='%schallenge?%s' % (server, query),
frame_url='%snoscript?%s' % (server, query),
options=json.dumps(options, cls=_JSONEncoder)
public_key=public_key
))

def __call__(self, field, error=None, **kwargs):
Expand All @@ -52,32 +48,5 @@ def __call__(self, field, error=None, **kwargs):
public_key = current_app.config['RECAPTCHA_PUBLIC_KEY']
except KeyError:
raise RuntimeError("RECAPTCHA_PUBLIC_KEY config not set")
query_options = dict(k=public_key)

if field.recaptcha_error is not None:
query_options['error'] = text_type(field.recaptcha_error)

query = url_encode(query_options)

_ = field.gettext

options = {
'theme': 'clean',
'custom_translations': {
'audio_challenge': _('Get an audio challenge'),
'cant_hear_this': _('Download sound as MP3'),
'help_btn': _('Help'),
'image_alt_text': _('reCAPTCHA challenge image'),
'incorrect_try_again': _('Incorrect. Try again.'),
'instructions_audio': _('Type what you hear'),
'instructions_visual': _('Type the text'),
'play_again': _('Play sound again'),
'privacy_and_terms': _('Privacy & Terms'),
'refresh_btn': _('Get a new challenge'),
'visual_challenge': _('Get a visual challenge'),
}
}

options.update(current_app.config.get('RECAPTCHA_OPTIONS', {}))

return self.recaptcha_html(query, options)
return self.recaptcha_html(public_key)

0 comments on commit 92c6e39

Please sign in to comment.