diff --git a/docs/configuration.rst b/docs/configuration.rst index d29ef9f1..140a12e6 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -155,6 +155,11 @@ Note: this setting is actually interpreted by Flask-Babel, see the .. _Flask-Babel guide for formatting dates: https://pythonhosted.org/Flask-Babel/#formatting-dates +`ENABLE_CAPTCHA` +--------------- + +It is possible to add a simple captcha in order to filter out spammer bots on the form creation. +In order to do so, you just have to set `ENABLE_CAPTCHA = True`. Configuring emails sending -------------------------- diff --git a/ihatemoney/conf-templates/ihatemoney.cfg.j2 b/ihatemoney/conf-templates/ihatemoney.cfg.j2 index 13a8e9f5..9d117c18 100644 --- a/ihatemoney/conf-templates/ihatemoney.cfg.j2 +++ b/ihatemoney/conf-templates/ihatemoney.cfg.j2 @@ -42,3 +42,7 @@ ACTIVATE_ADMIN_DASHBOARD = False # Enable secure cookies. Requires HTTPS. Disable if you run your ihatemoney # service over plain HTTP. SESSION_COOKIE_SECURE = True + +# You can activate an optional CAPTCHA if you want to. It can be helpful +# to filter spammer bots. +# ENABLE_CAPTCHA = True diff --git a/ihatemoney/default_settings.py b/ihatemoney/default_settings.py index 96795a01..3204ed3b 100644 --- a/ihatemoney/default_settings.py +++ b/ihatemoney/default_settings.py @@ -32,3 +32,4 @@ SUPPORTED_LANGUAGES = [ "uk", "zh_Hans", ] +ENABLE_CAPTCHA = False diff --git a/ihatemoney/forms.py b/ihatemoney/forms.py index 180619c7..3bbe34a5 100644 --- a/ihatemoney/forms.py +++ b/ihatemoney/forms.py @@ -221,6 +221,19 @@ class ProjectForm(EditProjectForm): ) raise ValidationError(Markup(message)) + @classmethod + def enable_captcha(cls): + captchaField = StringField( + _("Which is a real currency: Euro or Petro dollar?"), + validators=[DataRequired()], + ) + setattr(cls, "captcha", captchaField) + + def validate_captcha(form, field): + if not field.data.lower() == _("euro"): + message = _("Please, validate the captcha to proceed.") + raise ValidationError(Markup(message)) + class DestructiveActionProjectForm(FlaskForm): """Used for any important "delete" action linked to a project: diff --git a/ihatemoney/templates/forms.html b/ihatemoney/templates/forms.html index a9965564..9d8e8c99 100644 --- a/ihatemoney/templates/forms.html +++ b/ihatemoney/templates/forms.html @@ -75,6 +75,9 @@ {{ input(form.name) }} {{ input(form.password) }} {{ input(form.contact_email) }} + {% if config['ENABLE_CAPTCHA'] %} + {{ input(form.captcha) }} + {% endif %} {{ input(form.default_currency) }} {% if not home %} {{ submit(form.submit, home=True) }} @@ -171,7 +174,7 @@ - +
{{ _("More options") }} {% if g.project.default_currency != "XXX" %} diff --git a/ihatemoney/tests/common/ihatemoney_testcase.py b/ihatemoney/tests/common/ihatemoney_testcase.py index 4a92f616..2c34e1e1 100644 --- a/ihatemoney/tests/common/ihatemoney_testcase.py +++ b/ihatemoney/tests/common/ihatemoney_testcase.py @@ -15,6 +15,7 @@ class BaseTestCase(TestCase): SQLALCHEMY_DATABASE_URI = os.environ.get( "TESTING_SQLALCHEMY_DATABASE_URI", "sqlite://" ) + ENABLE_CAPTCHA = False def create_app(self): # Pass the test object as a configuration. diff --git a/ihatemoney/tests/main_test.py b/ihatemoney/tests/main_test.py index eaf6a01f..604aedd2 100644 --- a/ihatemoney/tests/main_test.py +++ b/ihatemoney/tests/main_test.py @@ -31,6 +31,7 @@ class ConfigurationTestCase(BaseTestCase): self.assertTrue(self.app.config["ACTIVATE_DEMO_PROJECT"]) self.assertTrue(self.app.config["ALLOW_PUBLIC_PROJECT_CREATION"]) self.assertFalse(self.app.config["ACTIVATE_ADMIN_DASHBOARD"]) + self.assertFalse(self.app.config["ENABLE_CAPTCHA"]) def test_env_var_configuration_file(self): """Test that settings are loaded from a configuration file specified @@ -241,6 +242,50 @@ class EmailFailureTestCase(IhatemoneyTestCase): ) +class CaptchaTestCase(IhatemoneyTestCase): + ENABLE_CAPTCHA = True + + def test_project_creation_with_captcha(self): + with self.app.test_client() as c: + c.post( + "/create", + data={ + "name": "raclette party", + "id": "raclette", + "password": "party", + "contact_email": "raclette@notmyidea.org", + "default_currency": "USD", + }, + ) + assert len(models.Project.query.all()) == 0 + + c.post( + "/create", + data={ + "name": "raclette party", + "id": "raclette", + "password": "party", + "contact_email": "raclette@notmyidea.org", + "default_currency": "USD", + "captcha": "nope", + }, + ) + assert len(models.Project.query.all()) == 0 + + c.post( + "/create", + data={ + "name": "raclette party", + "id": "raclette", + "password": "party", + "contact_email": "raclette@notmyidea.org", + "default_currency": "USD", + "captcha": "euro", + }, + ) + assert len(models.Project.query.all()) == 1 + + class TestCurrencyConverter(unittest.TestCase): converter = CurrencyConverter() mock_data = {"USD": 1, "EUR": 0.8, "CAD": 1.2, CurrencyConverter.no_currency: 1} diff --git a/ihatemoney/web.py b/ihatemoney/web.py index 5af15e08..19f88976 100644 --- a/ihatemoney/web.py +++ b/ihatemoney/web.py @@ -260,9 +260,16 @@ def authenticate(project_id=None): return render_template("authenticate.html", form=form) +def get_project_form(): + if current_app.config.get("ENABLE_CAPTCHA", False): + ProjectForm.enable_captcha() + + return ProjectForm() + + @main.route("/", strict_slashes=False) def home(): - project_form = ProjectForm() + project_form = get_project_form() auth_form = AuthenticationForm() is_demo_project_activated = current_app.config["ACTIVATE_DEMO_PROJECT"] is_public_project_creation_allowed = current_app.config[ @@ -287,7 +294,7 @@ def mobile(): @main.route("/create", methods=["GET", "POST"]) @requires_admin(bypass=("ALLOW_PUBLIC_PROJECT_CREATION", True)) def create_project(): - form = ProjectForm() + form = get_project_form() if request.method == "GET" and "project_id" in request.values: form.name.data = request.values["project_id"]