diff --git a/docs/changes.rst b/docs/changes.rst index 9cb3a24a..54fc1aac 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -5,6 +5,8 @@ Version 1.0.0 -------------- - Deprecated items removal :pr:`484` +- Support for alternatives captcha services :pr:`425` :pr:`342` + :pr:`387` :issue:`384` Version 0.15.1 -------------- diff --git a/docs/config.rst b/docs/config.rst index 79c64234..b4decaed 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -33,16 +33,29 @@ Configuration Recaptcha --------- -========================= ============================================== -``RECAPTCHA_PUBLIC_KEY`` **required** A public key. -``RECAPTCHA_PRIVATE_KEY`` **required** A private key. - https://www.google.com/recaptcha/admin -``RECAPTCHA_PARAMETERS`` **optional** A dict of configuration options. -``RECAPTCHA_HTML`` **optional** Override default HTML template - for Recaptcha. -``RECAPTCHA_DATA_ATTRS`` **optional** A dict of ``data-`` attrs to use - for Recaptcha div -========================= ============================================== +=========================== ============================================== +``RECAPTCHA_PUBLIC_KEY`` **required** A public key. +``RECAPTCHA_PRIVATE_KEY`` **required** A private key. + https://www.google.com/recaptcha/admin +``RECAPTCHA_PARAMETERS`` **optional** A dict of configuration options. +``RECAPTCHA_HTML`` **optional** Override default HTML template + for Recaptcha. +``RECAPTCHA_DATA_ATTRS`` **optional** A dict of ``data-`` attrs to use + for Recaptcha div +``RECAPTCHA_SCRIPT`` **optional** Override the default captcha + script URI in case an alternative service to + reCAPtCHA, e.g. hCaptcha is used. Default is + ``'https://www.google.com/recaptcha/api.js'`` +``RECAPTCHA_DIV_CLASS`` **optional** Override the default class of the + captcha div in case an alternative captcha + service is used. Default is + ``'g-recaptcha'`` +``RECAPTCHA_VERIFY_SERVER`` **optional** Override the default verification + server in case an alternative service is used. + Default is + ``'https://www.google.com/recaptcha/api/siteverify'`` + +=========================== ============================================== Logging ------- diff --git a/src/flask_wtf/recaptcha/validators.py b/src/flask_wtf/recaptcha/validators.py index c3e92309..10712d9c 100644 --- a/src/flask_wtf/recaptcha/validators.py +++ b/src/flask_wtf/recaptcha/validators.py @@ -6,7 +6,7 @@ from werkzeug.urls import url_encode from wtforms import ValidationError -RECAPTCHA_VERIFY_SERVER = "https://www.google.com/recaptcha/api/siteverify" +RECAPTCHA_VERIFY_SERVER_DEFAULT = "https://www.google.com/recaptcha/api/siteverify" RECAPTCHA_ERROR_CODES = { "missing-input-secret": "The secret parameter is missing.", "invalid-input-secret": "The secret parameter is invalid or malformed.", @@ -50,11 +50,15 @@ def _validate_recaptcha(self, response, remote_addr): except KeyError: raise RuntimeError("No RECAPTCHA_PRIVATE_KEY config set") from None + verify_server = current_app.config.get("RECAPTCHA_VERIFY_SERVER") + if not verify_server: + verify_server = RECAPTCHA_VERIFY_SERVER_DEFAULT + data = url_encode( {"secret": private_key, "remoteip": remote_addr, "response": response} ) - http_response = http.urlopen(RECAPTCHA_VERIFY_SERVER, data.encode("utf-8")) + http_response = http.urlopen(verify_server, data.encode("utf-8")) if http_response.code != 200: return False diff --git a/src/flask_wtf/recaptcha/widgets.py b/src/flask_wtf/recaptcha/widgets.py index 0e0affbd..7dc65071 100644 --- a/src/flask_wtf/recaptcha/widgets.py +++ b/src/flask_wtf/recaptcha/widgets.py @@ -5,10 +5,11 @@ JSONEncoder = json.JSONEncoder -RECAPTCHA_SCRIPT = "https://www.google.com/recaptcha/api.js" +RECAPTCHA_SCRIPT_DEFAULT = "https://www.google.com/recaptcha/api.js" +RECAPTCHA_DIV_CLASS_DEFAULT = "g-recaptcha" RECAPTCHA_TEMPLATE = """ -
+
""" __all__ = ["RecaptchaWidget"] @@ -20,14 +21,18 @@ def recaptcha_html(self, public_key): if html: return Markup(html) params = current_app.config.get("RECAPTCHA_PARAMETERS") - script = RECAPTCHA_SCRIPT + script = current_app.config.get("RECAPTCHA_SCRIPT") + if not script: + script = RECAPTCHA_SCRIPT_DEFAULT if params: script += "?" + url_encode(params) - attrs = current_app.config.get("RECAPTCHA_DATA_ATTRS", {}) attrs["sitekey"] = public_key snippet = " ".join(f'data-{k}="{attrs[k]}"' for k in attrs) - return Markup(RECAPTCHA_TEMPLATE % (script, snippet)) + div_class = current_app.config.get("RECAPTCHA_DIV_CLASS") + if not div_class: + div_class = RECAPTCHA_DIV_CLASS_DEFAULT + return Markup(RECAPTCHA_TEMPLATE % (script, div_class, snippet)) def __call__(self, field, error=None, **kwargs): """Returns the recaptcha input HTML.""" diff --git a/tests/test_recaptcha.py b/tests/test_recaptcha.py index 483b5217..f1ed5b14 100644 --- a/tests/test_recaptcha.py +++ b/tests/test_recaptcha.py @@ -51,6 +51,14 @@ def test_render_has_js(): assert "https://www.google.com/recaptcha/api.js" in render +def test_render_has_custom_js(app): + captcha_script = "https://hcaptcha.com/1/api.js" + app.config["RECAPTCHA_SCRIPT"] = captcha_script + f = RecaptchaForm() + render = f.recaptcha() + assert captcha_script in render + + def test_render_custom_html(app): app.config["RECAPTCHA_HTML"] = "custom" f = RecaptchaForm() @@ -59,6 +67,14 @@ def test_render_custom_html(app): assert isinstance(render, Markup) +def test_render_custom_div_class(app): + div_class = "h-captcha" + app.config["RECAPTCHA_DIV_CLASS"] = div_class + f = RecaptchaForm() + render = f.recaptcha() + assert div_class in render + + def test_render_custom_args(app): app.config["RECAPTCHA_PARAMETERS"] = {"key": "(value)"} app.config["RECAPTCHA_DATA_ATTRS"] = {"red": "blue"} @@ -135,6 +151,20 @@ def mock_urlopen(url, data): assert not f.recaptcha.errors +def test_request_custom_verify_server(app, monkeypatch): + verify_server = "https://hcaptcha.com/siteverify" + + def mock_urlopen(url, data): + assert url == verify_server + return MockResponse(200, "") + + monkeypatch.setattr(http, "urlopen", mock_urlopen) + app.config["RECAPTCHA_VERIFY_SERVER"] = verify_server + f = RecaptchaForm() + f.validate() + assert not f.recaptcha.errors + + def test_request_unmatched_error(monkeypatch): def mock_urlopen(url, data): return MockResponse(200, "not-an-error", True)