diff --git a/cloud_captchas/MANIFEST.in b/cloud_captchas/MANIFEST.in new file mode 100644 index 0000000..fc89d1d --- /dev/null +++ b/cloud_captchas/MANIFEST.in @@ -0,0 +1,4 @@ +graft indico_cloud_captchas/static +graft indico_cloud_captchas/translations + +global-exclude *.pyc __pycache__ .keep diff --git a/cloud_captchas/README.md b/cloud_captchas/README.md new file mode 100644 index 0000000..e6856f3 --- /dev/null +++ b/cloud_captchas/README.md @@ -0,0 +1,27 @@ +# Cloud CAPTCHAs Plugin + +This plugin replaces the built-in CAPTCHA with Google's reCAPTCHA v2 or hCaptcha. + +This plugin also serves as an example for developers who want to use a different +CAPTCHA for their Indico instance. + +![screenshot](recaptcha.png) + + +## Setup + +The plugin requires you to set the site key and secret key on the plugin settings page. + +When using reCaptcha these keys can be created on the [reCAPTCHA admin dashboard][recaptcha-create]. +Choose **reCAPTCHA v2** and **"I'm not a robot" Checkbox**. + +When using hCaptcha the keys can be created on the [hCaptcha dashboard][hcaptcha-dashboard]. + +## Changelog + +### 3.2 + +- Initial release for Indico 3.2 + +[recaptcha-create]: https://www.google.com/recaptcha/admin/create +[hcaptcha-dashboard]: https://dashboard.hcaptcha.com/overview diff --git a/recaptcha/indico_recaptcha/__init__.py b/cloud_captchas/indico_cloud_captchas/__init__.py similarity index 87% rename from recaptcha/indico_recaptcha/__init__.py rename to cloud_captchas/indico_cloud_captchas/__init__.py index 1e789e6..31a28af 100644 --- a/recaptcha/indico_recaptcha/__init__.py +++ b/cloud_captchas/indico_cloud_captchas/__init__.py @@ -8,4 +8,4 @@ from indico.util.i18n import make_bound_gettext -_ = make_bound_gettext('recaptcha') +_ = make_bound_gettext('cloud_captchas') diff --git a/recaptcha/indico_recaptcha/client/Captcha.jsx b/cloud_captchas/indico_cloud_captchas/client/CloudCaptcha.jsx similarity index 71% rename from recaptcha/indico_recaptcha/client/Captcha.jsx rename to cloud_captchas/indico_cloud_captchas/client/CloudCaptcha.jsx index 6896325..110b41f 100644 --- a/recaptcha/indico_recaptcha/client/Captcha.jsx +++ b/cloud_captchas/indico_cloud_captchas/client/CloudCaptcha.jsx @@ -5,6 +5,7 @@ // them and/or modify them under the terms of the MIT License; // see the LICENSE file for more details. +import HCaptcha from '@hcaptcha/react-hcaptcha'; import PropTypes from 'prop-types'; import React, {useState, useEffect, useRef, useCallback} from 'react'; import {useFormState, useForm} from 'react-final-form'; @@ -14,44 +15,55 @@ import {Message, Form} from 'semantic-ui-react'; import {FinalField} from 'indico/react/forms'; import {Translate} from 'indico/react/i18n'; -import './Captcha.module.scss'; +import './CloudCaptcha.module.scss'; -export default function Captcha({name, settings: {siteKey}, wtf}) { +export default function CloudCaptcha({name, settings: {siteKey, hCaptcha}, wtf}) { return wtf ? ( - + ) : ( - + ); } -Captcha.propTypes = { +CloudCaptcha.propTypes = { name: PropTypes.string, wtf: PropTypes.bool, settings: PropTypes.shape({ siteKey: PropTypes.string.isRequired, + hCaptcha: PropTypes.bool.isRequired, }).isRequired, }; -Captcha.defaultProps = { +CloudCaptcha.defaultProps = { name: 'captcha', wtf: false, }; -function CaptchaField({onChange, siteKey, reCaptchaRef}) { - return ; +function CloudCaptchaField({onChange, siteKey, hCaptcha, reCaptchaRef}) { + return hCaptcha ? ( + onChange(null)} + ref={reCaptchaRef} + /> + ) : ( + + ); } -CaptchaField.propTypes = { +CloudCaptchaField.propTypes = { onChange: PropTypes.func.isRequired, siteKey: PropTypes.string.isRequired, + hCaptcha: PropTypes.bool.isRequired, reCaptchaRef: PropTypes.object, }; -CaptchaField.defaultProps = { +CloudCaptchaField.defaultProps = { reCaptchaRef: undefined, }; -function WTFCaptcha({name, siteKey}) { +function WTFCloudCaptcha({name, siteKey, hCaptcha}) { const fieldRef = useRef(null); const [response, setResponse] = useState(''); const [hasError, setError] = useState(false); @@ -88,7 +100,7 @@ function WTFCaptcha({name, siteKey}) {
- +
@@ -96,12 +108,13 @@ function WTFCaptcha({name, siteKey}) { ); } -WTFCaptcha.propTypes = { +WTFCloudCaptcha.propTypes = { name: PropTypes.string.isRequired, siteKey: PropTypes.string.isRequired, + hCaptcha: PropTypes.bool.isRequired, }; -function FinalCaptcha({name, siteKey}) { +function FinalCloudCaptcha({name, siteKey, hCaptcha}) { const reCaptchaRef = useRef(null); const form = useForm(); const {submitErrors} = useFormState({ @@ -132,8 +145,9 @@ function FinalCaptcha({name, siteKey}) { @@ -142,7 +156,8 @@ function FinalCaptcha({name, siteKey}) { ); } -FinalCaptcha.propTypes = { +FinalCloudCaptcha.propTypes = { name: PropTypes.string.isRequired, siteKey: PropTypes.string.isRequired, + hCaptcha: PropTypes.bool.isRequired, }; diff --git a/recaptcha/indico_recaptcha/client/Captcha.module.scss b/cloud_captchas/indico_cloud_captchas/client/CloudCaptcha.module.scss similarity index 100% rename from recaptcha/indico_recaptcha/client/Captcha.module.scss rename to cloud_captchas/indico_cloud_captchas/client/CloudCaptcha.module.scss diff --git a/recaptcha/indico_recaptcha/client/i18n.js b/cloud_captchas/indico_cloud_captchas/client/i18n.js similarity index 83% rename from recaptcha/indico_recaptcha/client/i18n.js rename to cloud_captchas/indico_cloud_captchas/client/i18n.js index c4abf17..dd00712 100644 --- a/recaptcha/indico_recaptcha/client/i18n.js +++ b/cloud_captchas/indico_cloud_captchas/client/i18n.js @@ -7,7 +7,7 @@ import {bindTranslateComponents} from 'indico/react/i18n'; -const {Translate, PluralTranslate} = bindTranslateComponents('cern_access'); +const {Translate, PluralTranslate} = bindTranslateComponents('cloud_captchas'); export {Translate, PluralTranslate}; export {Singular, Plural, Param} from 'react-jsx-i18n'; diff --git a/recaptcha/indico_recaptcha/client/index.js b/cloud_captchas/indico_cloud_captchas/client/index.js similarity index 73% rename from recaptcha/indico_recaptcha/client/index.js rename to cloud_captchas/indico_cloud_captchas/client/index.js index 5531e50..8c86ec6 100644 --- a/recaptcha/indico_recaptcha/client/index.js +++ b/cloud_captchas/indico_cloud_captchas/client/index.js @@ -7,6 +7,6 @@ import {registerPluginComponent} from 'indico/utils/plugins'; -import Captcha from './Captcha'; +import CloudCaptcha from './CloudCaptcha'; -registerPluginComponent('recaptcha', 'captcha', Captcha); +registerPluginComponent('cloud_captchas', 'captcha', CloudCaptcha); diff --git a/cloud_captchas/indico_cloud_captchas/plugin.py b/cloud_captchas/indico_cloud_captchas/plugin.py new file mode 100644 index 0000000..b378236 --- /dev/null +++ b/cloud_captchas/indico_cloud_captchas/plugin.py @@ -0,0 +1,143 @@ +# This file is part of the Indico plugins. +# Copyright (C) 2002 - 2022 CERN +# +# The Indico plugins are free software; you can redistribute +# them and/or modify them under the terms of the MIT License; +# see the LICENSE file for more details. + +import requests +from requests.exceptions import HTTPError, RequestException +from wtforms.fields import StringField +from wtforms.validators import DataRequired + +from indico.core.plugins import IndicoPlugin +from indico.modules.api.forms import IndicoEnumSelectField +from indico.modules.core.plugins import CaptchaPluginMixin +from indico.modules.users import EnumConverter +from indico.util.enum import RichIntEnum +from indico.web.forms.base import IndicoForm +from indico.web.forms.validators import HiddenUnless +from indico.web.views import WPBase + +from indico_cloud_captchas import _ + + +class CaptchaProvider(RichIntEnum): + __titles__ = [_("None (use Indico's built-in CAPTCHA)"), 'reCAPTCHA', 'hCaptcha'] + none = 0 + recaptcha = 1 + hcaptcha = 2 + + +class CloudCaptchasSettingsForm(IndicoForm): + provider = IndicoEnumSelectField( + _('Type'), enum=CaptchaProvider, + description=_('Select which CAPTCHA provider you want to use') + ) + # recaptcha + recaptcha_site_key = StringField( + _('reCAPTCHA site key'), + [HiddenUnless('provider', CaptchaProvider.recaptcha, preserve_data=True), DataRequired()], + description=_('The site key available in the reCAPTCHA admin dashboard') + ) + recaptcha_secret_key = StringField( + _('reCAPTCHA secret key'), + [HiddenUnless('provider', CaptchaProvider.recaptcha, preserve_data=True), DataRequired()], + description=_('The secret key available in the reCAPTCHA admin dashboard') + ) + # hcaptcha + hcaptcha_site_key = StringField( + _('hCaptcha site key'), + [HiddenUnless('provider', CaptchaProvider.hcaptcha, preserve_data=True), DataRequired()], + description=_('The site key available in the hCaptcha admin dashboard') + ) + hcaptcha_secret_key = StringField( + _('reCAPTCHA secret key'), + [HiddenUnless('provider', CaptchaProvider.hcaptcha, preserve_data=True), DataRequired()], + description=_('The secret key available in the hCaptcha admin dashboard') + ) + + +class CloudCaptchasPlugin(CaptchaPluginMixin, IndicoPlugin): + """Cloud CAPTCHAs + + Replaces Indico's default CAPTCHA with reCAPTCHA or hCaptcha. + """ + + configurable = True + settings_form = CloudCaptchasSettingsForm + default_settings = { + 'provider': CaptchaProvider.none, + 'recaptcha_site_key': '', + 'recaptcha_secret_key': '', + 'hcaptcha_site_key': '', + 'hcaptcha_secret_key': '', + } + settings_converters = { + 'provider': EnumConverter(CaptchaProvider), + } + + def init(self): + super().init() + # TODO split hcaptcha/recaptcha (hcaptcha is pretty big since it includes react) + self.inject_bundle('main.js', WPBase, condition=lambda: self.settings.get('provider') != CaptchaProvider.none) + self.inject_bundle('main.css', WPBase, condition=lambda: self.settings.get('provider') != CaptchaProvider.none) + + def is_captcha_available(self): + provider = self.settings.get('provider') + if provider == CaptchaProvider.recaptcha: + return bool(self.settings.get('recaptcha_site_key') and self.settings.get('recaptcha_secret_key')) + elif provider == CaptchaProvider.hcaptcha: + return bool(self.settings.get('hcaptcha_site_key') and self.settings.get('hcaptcha_secret_key')) + return False + + def _validate_recaptcha(self, answer): + data = { + 'secret': self.settings.get('recaptcha_secret_key'), + 'response': answer + } + if resp := self._validate_http_post('https://www.google.com/recaptcha/api/siteverify', data): + return resp.json()['success'] + return False + + def _validate_hcaptcha(self, answer): + data = { + 'sitekey': self.settings.get('hcaptcha_site_key'), + 'secret': self.settings.get('hcaptcha_secret_key'), + 'response': answer + } + if resp := self._validate_http_post('https://hcaptcha.com/siteverify', data): + return resp.json()['success'] + return False + + def _validate_http_post(self, url, data): + try: + resp = requests.post(url, data=data) + resp.raise_for_status() + except HTTPError as exc: + self.logger.error('Failed to validate CAPTCHA: %s', exc.response.text) + return None + except RequestException as exc: + self.logger.error('Failed to validate CAPTCHA: %s', exc) + return None + return resp + + def validate_captcha(self, answer): + if self.settings.get('provider') == CaptchaProvider.recaptcha: + return self._validate_recaptcha(answer) + elif self.settings.get('provider') == CaptchaProvider.hcaptcha: + return self._validate_hcaptcha(answer) + # should never happen + return False + + def get_captcha_settings(self): + if self.settings.get('provider') == CaptchaProvider.recaptcha: + return { + 'siteKey': self.settings.get('recaptcha_site_key'), + 'hCaptcha': False, + } + elif self.settings.get('provider') == CaptchaProvider.hcaptcha: + return { + 'siteKey': self.settings.get('hcaptcha_site_key'), + 'hCaptcha': True, + } diff --git a/recaptcha/package-lock.json b/cloud_captchas/package-lock.json similarity index 63% rename from recaptcha/package-lock.json rename to cloud_captchas/package-lock.json index 4ba4adb..d35fa9f 100644 --- a/recaptcha/package-lock.json +++ b/cloud_captchas/package-lock.json @@ -1,16 +1,38 @@ { - "name": "recaptcha", - "version": "1.0.0", + "name": "indico-plugin-cloud-captchas", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "recaptcha", - "version": "1.0.0", + "name": "indico-plugin-cloud-captchas", "dependencies": { + "@hcaptcha/react-hcaptcha": "^1.4.4", "react-google-recaptcha": "^2.1.0" } }, + "node_modules/@babel/runtime": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.9.tgz", + "integrity": "sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==", + "dependencies": { + "regenerator-runtime": "^0.13.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@hcaptcha/react-hcaptcha": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@hcaptcha/react-hcaptcha/-/react-hcaptcha-1.4.4.tgz", + "integrity": "sha512-Aen217LDnf5ywbPSwBG5CsoqBLIHIAS9lhj3zQjXJuO13doQ6/ubkCWNuY8jmwYLefoFt3V3MrZmCdKDaFoTuQ==", + "dependencies": { + "@babel/runtime": "^7.17.9" + }, + "peerDependencies": { + "react": ">= 16.3.0", + "react-dom": ">= 16.3.0" + } + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -77,6 +99,19 @@ "react": ">=16.4.1" } }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, "node_modules/react-google-recaptcha": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/react-google-recaptcha/-/react-google-recaptcha-2.1.0.tgz", @@ -93,9 +128,39 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/regenerator-runtime": { + "version": "0.13.9", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", + "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" + }, + "node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + } } }, "dependencies": { + "@babel/runtime": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.18.9.tgz", + "integrity": "sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "@hcaptcha/react-hcaptcha": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@hcaptcha/react-hcaptcha/-/react-hcaptcha-1.4.4.tgz", + "integrity": "sha512-Aen217LDnf5ywbPSwBG5CsoqBLIHIAS9lhj3zQjXJuO13doQ6/ubkCWNuY8jmwYLefoFt3V3MrZmCdKDaFoTuQ==", + "requires": { + "@babel/runtime": "^7.17.9" + } + }, "hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -150,6 +215,16 @@ "prop-types": "^15.5.0" } }, + "react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "peer": true, + "requires": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + } + }, "react-google-recaptcha": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/react-google-recaptcha/-/react-google-recaptcha-2.1.0.tgz", @@ -163,6 +238,20 @@ "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "regenerator-runtime": { + "version": "0.13.9", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", + "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" + }, + "scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "peer": true, + "requires": { + "loose-envify": "^1.1.0" + } } } } diff --git a/cloud_captchas/package.json b/cloud_captchas/package.json new file mode 100644 index 0000000..7e2ee0d --- /dev/null +++ b/cloud_captchas/package.json @@ -0,0 +1,8 @@ +{ + "private": true, + "name": "indico-plugin-cloud-captchas", + "dependencies": { + "@hcaptcha/react-hcaptcha": "^1.4.4", + "react-google-recaptcha": "^2.1.0" + } +} diff --git a/recaptcha/recaptcha.png b/cloud_captchas/recaptcha.png similarity index 100% rename from recaptcha/recaptcha.png rename to cloud_captchas/recaptcha.png diff --git a/recaptcha/setup.cfg b/cloud_captchas/setup.cfg similarity index 87% rename from recaptcha/setup.cfg rename to cloud_captchas/setup.cfg index 428af37..5b80e78 100644 --- a/recaptcha/setup.cfg +++ b/cloud_captchas/setup.cfg @@ -1,5 +1,5 @@ [metadata] -name = indico-plugin-recaptcha +name = indico-plugin-cloud-captchas version = 3.2-dev description = Google reCAPTCHA plugin for Indico long_description = file: README.md @@ -24,7 +24,7 @@ install_requires = [options.entry_points] indico.plugins = - recaptcha = indico_recaptcha.plugin:ReCaptchaPlugin + cloud_captchas = indico_cloud_captchas.plugin:CloudCaptchasPlugin diff --git a/recaptcha/setup.py b/cloud_captchas/setup.py similarity index 100% rename from recaptcha/setup.py rename to cloud_captchas/setup.py diff --git a/recaptcha/webpack-bundles.json b/cloud_captchas/webpack-bundles.json similarity index 100% rename from recaptcha/webpack-bundles.json rename to cloud_captchas/webpack-bundles.json diff --git a/recaptcha/MANIFEST.in b/recaptcha/MANIFEST.in deleted file mode 100644 index a5c60ec..0000000 --- a/recaptcha/MANIFEST.in +++ /dev/null @@ -1,4 +0,0 @@ -graft indico_recaptcha/static -graft indico_recaptcha/translations - -global-exclude *.pyc __pycache__ .keep diff --git a/recaptcha/README.md b/recaptcha/README.md deleted file mode 100644 index 4f0ed09..0000000 --- a/recaptcha/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# Google reCAPTCHA Plugin - -This plugin replaces the built-in CAPTCHA with Google's reCAPTCHA v2. - -This plugin also serves as an example for developers who want to use a -different CAPTCHA for their Indico instance. - -![screenshot](recaptcha.png) - - -## Setup - -The plugin requires you to set the reCAPTCHA site key and secret key on the plugin -settings page. These keys can be created on the [reCAPTCHA admin dashboard][recaptcha-create]. -Choose **reCAPTCHA v2** and **"I'm not a robot" Checkbox**. - -## Changelog - -### 3.2 - -- Initial release for Indico 3.2 - -[recaptcha-create]: https://www.google.com/recaptcha/admin/create diff --git a/recaptcha/indico_recaptcha/plugin.py b/recaptcha/indico_recaptcha/plugin.py deleted file mode 100644 index 0250a64..0000000 --- a/recaptcha/indico_recaptcha/plugin.py +++ /dev/null @@ -1,64 +0,0 @@ -# This file is part of the Indico plugins. -# Copyright (C) 2002 - 2022 CERN -# -# The Indico plugins are free software; you can redistribute -# them and/or modify them under the terms of the MIT License; -# see the LICENSE file for more details. - -import requests -from requests.exceptions import RequestException -from wtforms.fields import BooleanField, StringField -from wtforms.validators import DataRequired - -from indico.core.plugins import IndicoPlugin -from indico.modules.core.plugins import CaptchaPluginMixin -from indico.web.forms.base import IndicoForm -from indico.web.forms.widgets import SwitchWidget -from indico.web.views import WPBase - -from indico_recaptcha import _ - - -class ReCaptchaSettingsForm(IndicoForm): - enabled = BooleanField(_('Enabled'), widget=SwitchWidget(), description=_('Whether to enable the access overrides')) - site_key = StringField(_('Site key'), [DataRequired()], - description=_('The site key available in the reCAPTCHA admin dashboard')) - secret_key = StringField(_('Secret key'), [DataRequired()], - description=_('The secret key available in the reCAPTCHA admin dashboard')) - - -class ReCaptchaPlugin(CaptchaPluginMixin, IndicoPlugin): - """Google reCAPTCHA - - Replaces Indico's default CAPTCHA with Google reCAPTCHA. - """ - - configurable = True - settings_form = ReCaptchaSettingsForm - default_settings = { - 'enabled': False, - 'site_key': '', - 'secret_key': '', - } - - def init(self): - super().init() - self.inject_bundle('main.js', WPBase, condition=lambda: self.settings.get('enabled')) - self.inject_bundle('main.css', WPBase, condition=lambda: self.settings.get('enabled')) - - def is_captcha_available(self): - return self.settings.get('enabled') and bool(self.settings.get('site_key')) - - def validate_captcha(self, answer): - secret = self.settings.get('secret_key') - resp = requests.post('https://www.google.com/recaptcha/api/siteverify', - data={'secret': secret, 'response': answer}) - try: - resp.raise_for_status() - except RequestException as exc: - self.logger.error('Failed to validate CAPTCHA: %s', exc.response.text) - return False - return resp.json()['success'] - - def get_captcha_settings(self): - return {'siteKey': self.settings.get('site_key')} diff --git a/recaptcha/package.json b/recaptcha/package.json deleted file mode 100644 index 9dbd806..0000000 --- a/recaptcha/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "recaptcha", - "private": true, - "version": "1.0.0", - "repository": "https://github.com/indico/indico-plugins", - "author": "Indico Team ", - "dependencies": { - "react-google-recaptcha": "^2.1.0" - } -}