From 92c6e3953c3bdf2ff3cf03c1216648e6e0b77a0a Mon Sep 17 00:00:00 2001 From: Maximilian Berger Date: Fri, 5 Dec 2014 13:50:24 +0100 Subject: [PATCH 1/5] Use reCaptcha api v2 No need to change existing code, this just works. --- flask_wtf/_compat.py | 7 +++ flask_wtf/recaptcha/validators.py | 46 ++++++++---------- flask_wtf/recaptcha/widgets.py | 79 ++++++++++--------------------- 3 files changed, 50 insertions(+), 82 deletions(-) diff --git a/flask_wtf/_compat.py b/flask_wtf/_compat.py index 7295b737..d3f657d6 100644 --- a/flask_wtf/_compat.py +++ b/flask_wtf/_compat.py @@ -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 diff --git a/flask_wtf/recaptcha/validators.py b/flask_wtf/recaptcha/validators.py index 1906933a..7d3a6cb4 100644 --- a/flask_wtf/recaptcha/validators.py +++ b/flask_wtf/recaptcha/validators.py @@ -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.'): @@ -38,21 +34,19 @@ 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'] @@ -60,24 +54,22 @@ def _validate_recaptcha(self, challenge, response, remote_addr): 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]) diff --git a/flask_wtf/recaptcha/widgets.py b/flask_wtf/recaptcha/widgets.py index 9662a384..b7935bc9 100644 --- a/flask_wtf/recaptcha/widgets.py +++ b/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''' - - + +
''' @@ -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): @@ -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) From 3501ad8b71dd49d1f8facf8d0dcc82691bc2293e Mon Sep 17 00:00:00 2001 From: Maximilian Berger Date: Fri, 5 Dec 2014 15:23:30 +0100 Subject: [PATCH 2/5] PEP8: Just use one space --- flask_wtf/recaptcha/validators.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/flask_wtf/recaptcha/validators.py b/flask_wtf/recaptcha/validators.py index 7d3a6cb4..e3c8b9e8 100644 --- a/flask_wtf/recaptcha/validators.py +++ b/flask_wtf/recaptcha/validators.py @@ -20,10 +20,10 @@ class Recaptcha(object): """Validates a ReCaptcha.""" _error_codes = { - '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.', + '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.'): From 2b677535a51dc0eac0ea7883999d9922902d2119 Mon Sep 17 00:00:00 2001 From: Maximilian Berger Date: Fri, 5 Dec 2014 15:26:54 +0100 Subject: [PATCH 3/5] Renamed `from_bytes` to `to_unicode` --- flask_wtf/_compat.py | 2 +- flask_wtf/recaptcha/validators.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/flask_wtf/_compat.py b/flask_wtf/_compat.py index d3f657d6..9f7da0ff 100644 --- a/flask_wtf/_compat.py +++ b/flask_wtf/_compat.py @@ -14,7 +14,7 @@ def to_bytes(text): return text -def from_bytes(bytes, encoding='utf-8'): +def to_unicode(bytes, encoding='utf-8'): """Decodes bytes to text if needed.""" if not isinstance(bytes, string_types): bytes = bytes.decode(encoding) diff --git a/flask_wtf/recaptcha/validators.py b/flask_wtf/recaptcha/validators.py index e3c8b9e8..061c18bf 100644 --- a/flask_wtf/recaptcha/validators.py +++ b/flask_wtf/recaptcha/validators.py @@ -7,7 +7,7 @@ from flask import request, current_app from wtforms import ValidationError from werkzeug import url_encode -from .._compat import to_bytes, from_bytes +from .._compat import to_bytes, to_unicode import json RECAPTCHA_VERIFY_SERVER = 'https://www.google.com/recaptcha/api/siteverify' @@ -64,7 +64,7 @@ def _validate_recaptcha(self, response, remote_addr): if http_response.code != 200: return False - json_resp = json.loads(from_bytes(http_response.read())) + json_resp = json.loads(to_unicode(http_response.read())) if json_resp["success"]: return True From f2dc22e305a71efd7bc55abe84489819cc5d4b0c Mon Sep 17 00:00:00 2001 From: Maximilian Berger Date: Fri, 5 Dec 2014 15:29:13 +0100 Subject: [PATCH 4/5] Renamed function parameter of `to_unicode` From `bytes` to `input_bytes` because `bytes` is a keyword. --- flask_wtf/_compat.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/flask_wtf/_compat.py b/flask_wtf/_compat.py index 9f7da0ff..bb6e9edd 100644 --- a/flask_wtf/_compat.py +++ b/flask_wtf/_compat.py @@ -14,8 +14,8 @@ def to_bytes(text): return text -def to_unicode(bytes, encoding='utf-8'): - """Decodes bytes to text if needed.""" - if not isinstance(bytes, string_types): - bytes = bytes.decode(encoding) - return bytes +def to_unicode(input_bytes, encoding='utf-8'): + """Decodes input_bytes to text if needed.""" + if not isinstance(input_bytes, string_types): + input_bytes = input_bytes.decode(encoding) + return input_bytes From f5586b5e58508313702af2a429b7cd93cc314e68 Mon Sep 17 00:00:00 2001 From: Maximilian Berger Date: Fri, 5 Dec 2014 16:46:18 +0100 Subject: [PATCH 5/5] Removed `recaptcha_challenge_field` from tests --- tests/test_recaptcha.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/test_recaptcha.py b/tests/test_recaptcha.py index ae77f1bd..ef4fc71d 100644 --- a/tests/test_recaptcha.py +++ b/tests/test_recaptcha.py @@ -34,14 +34,12 @@ def test_recaptcha(self): response = self.client.get('/') assert b'//www.google.com/recaptcha/api/' in response.data - def test_invalid_recaptcha(self): response = self.client.post('/', data={}) assert b'Invalid word' in response.data def test_send_recaptcha_request(self): response = self.client.post('/', data={ - 'recaptcha_challenge_field': 'test', 'recaptcha_response_field': 'test' }) assert b'Invalid word' in response.data @@ -49,7 +47,6 @@ def test_send_recaptcha_request(self): def test_testing(self): self.app.testing = True response = self.client.post('/', data={ - 'recaptcha_challenge_field': 'test', 'recaptcha_response_field': 'test' }) assert b'Invalid word' not in response.data @@ -57,7 +54,6 @@ def test_testing(self): def test_no_private_key(self): self.app.config.pop('RECAPTCHA_PRIVATE_KEY', None) response = self.client.post('/', data={ - 'recaptcha_challenge_field': 'test', 'recaptcha_response_field': 'test' }) assert response.status_code == 500