Skip to content

Commit

Permalink
Merge pull request #425 from Dosenpfand/hcaptcha
Browse files Browse the repository at this point in the history
Add support for alternative captcha services
  • Loading branch information
azmeuk committed Nov 7, 2021
2 parents 4b90067 + b7464f9 commit 746ccea
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 17 deletions.
2 changes: 2 additions & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
--------------
Expand Down
33 changes: 23 additions & 10 deletions docs/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
-------
Expand Down
8 changes: 6 additions & 2 deletions src/flask_wtf/recaptcha/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down Expand Up @@ -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
Expand Down
15 changes: 10 additions & 5 deletions src/flask_wtf/recaptcha/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = """
<script src='%s' async defer></script>
<div class="g-recaptcha" %s></div>
<div class="%s" %s></div>
"""

__all__ = ["RecaptchaWidget"]
Expand All @@ -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."""
Expand Down
30 changes: 30 additions & 0 deletions tests/test_recaptcha.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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"}
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit 746ccea

Please sign in to comment.