diff --git a/cms/sass/components/_alert.scss b/cms/sass/components/_alert.scss index 47f841bdb5..508f379fa8 100644 --- a/cms/sass/components/_alert.scss +++ b/cms/sass/components/_alert.scss @@ -3,7 +3,7 @@ .alert { display: inline-block; padding: $spacing-03; - margin: $spacing-02 0$spacing-03 0; + margin: $spacing-02 0 $spacing-03 0; background-color: $light-grey; border: 1px solid $warm-black; @include typescale-06; diff --git a/cms/sass/components/_honeypotfield.scss b/cms/sass/components/_honeypotfield.scss new file mode 100644 index 0000000000..7dc4df79de --- /dev/null +++ b/cms/sass/components/_honeypotfield.scss @@ -0,0 +1,11 @@ +.hpemail { + position: absolute; + left: -9999px; /* Large negative offset */ + top: -9999px; + height: 1px; + width: 1px; + overflow: hidden; + border: 0; + padding: 0; + margin: 0; +} \ No newline at end of file diff --git a/cms/sass/layout/_page-header.scss b/cms/sass/layout/_page-header.scss index 038f0dcf7d..d9b21d4a4e 100644 --- a/cms/sass/layout/_page-header.scss +++ b/cms/sass/layout/_page-header.scss @@ -7,7 +7,8 @@ min-height: 100px; } - a { + /* do not underline the links, unless in an alert */ + a:not(.alert a) { text-decoration: none; } diff --git a/cms/sass/main.scss b/cms/sass/main.scss index 0f72c8eac8..94636ffcfd 100644 --- a/cms/sass/main.scss +++ b/cms/sass/main.scss @@ -38,6 +38,7 @@ "components/filters", "components/form", "components/hero", + "components/honeypotfield", "components/input-group", "components/label", "components/loading", diff --git a/doajtest/fixtures/registrationForm.py b/doajtest/fixtures/registrationForm.py new file mode 100644 index 0000000000..c8c8ae7672 --- /dev/null +++ b/doajtest/fixtures/registrationForm.py @@ -0,0 +1,74 @@ +from werkzeug.datastructures import ImmutableMultiDict, CombinedMultiDict +from portality.core import app + +def get_longer_time_than_hp_threshold(): + return app.config.get("HONEYPOT_TIMER_THRESHOLD", 5000) + 100 + +def get_shorter_time_than_hp_threshold(): + return app.config.get("HONEYPOT_TIMER_THRESHOLD", 5000) - 100 + +COMMON = ImmutableMultiDict([ + ('next', '/register'), +]) + +VALID_FORM = ImmutableMultiDict([ + ('name', 'Aga'), + ('sender_email', 'aga@example.com'), +]) + +INVALID_FORM = ImmutableMultiDict([ + ('name', 'Aga'), + ('sender_email', ''), +]) + +VALID_HONEYPOT = ImmutableMultiDict([ + ('email', ''), + ('hptimer', get_longer_time_than_hp_threshold()) +]) + +INVALID_HONEYPOT_TIMER_BELOW_THRESHOLD = ImmutableMultiDict([ + ('email', ''), + ('hptimer', get_shorter_time_than_hp_threshold()) +]) + +INVALID_HONEYPOT_EMAIL_NOT_EMPTY = ImmutableMultiDict([ + ('email', 'this_field@should_be.empty'), + ('hptimer', get_shorter_time_than_hp_threshold()) +]) + +INVALID_HONEYPOT_BOTH_FIELDS = ImmutableMultiDict([ + ('email', 'this_field@should_be.empty'), + ('hptimer', get_longer_time_than_hp_threshold()) +]) + +# Method 1: Valid form with valid honeypot +def create_valid_form_with_valid_honeypot(): + return CombinedMultiDict([COMMON, VALID_FORM, VALID_HONEYPOT]) + +# Method 2: Valid form with invalid honeypot (timer exceeds threshold) +def create_valid_form_with_invalid_honeypot_timer_exceeds(): + return CombinedMultiDict([COMMON, VALID_FORM, INVALID_HONEYPOT_TIMER_BELOW_THRESHOLD]) + +# Method 3: Valid form with invalid honeypot (email not empty) +def create_valid_form_with_invalid_honeypot_email_not_empty(): + return CombinedMultiDict([COMMON, VALID_FORM, INVALID_HONEYPOT_EMAIL_NOT_EMPTY]) + +# Method 4: Invalid form with valid honeypot +def create_invalid_form_with_valid_honeypot(): + return CombinedMultiDict([COMMON, INVALID_FORM, VALID_HONEYPOT]) + +# Method 5: Invalid form with invalid honeypot (timer exceeds threshold) +def create_invalid_form_with_invalid_honeypot_timer_exceeds(): + return CombinedMultiDict([COMMON, INVALID_FORM, INVALID_HONEYPOT_TIMER_BELOW_THRESHOLD]) + +# Method 6: Invalid form with invalid honeypot (email not empty) +def create_invalid_form_with_invalid_honeypot_email_not_empty(): + return CombinedMultiDict([COMMON, INVALID_FORM, INVALID_HONEYPOT_EMAIL_NOT_EMPTY]) + +# Method 7: Valid form with invalid honeypot (both fields) +def create_valid_form_with_invalid_honeypot_both_fields(): + return CombinedMultiDict([COMMON, VALID_FORM, INVALID_HONEYPOT_BOTH_FIELDS]) + +# Method 8: Invalid form with invalid honeypot (both fields) +def create_invalid_form_with_invalid_honeypot_both_fields(): + return CombinedMultiDict([COMMON, INVALID_FORM, INVALID_HONEYPOT_BOTH_FIELDS]) diff --git a/doajtest/unit/test_honeypot.py b/doajtest/unit/test_honeypot.py new file mode 100644 index 0000000000..297206e5d3 --- /dev/null +++ b/doajtest/unit/test_honeypot.py @@ -0,0 +1,41 @@ +from doajtest.helpers import DoajTestCase +from doajtest.fixtures import registrationForm +from portality.view.account import RegisterForm +from werkzeug.datastructures import ImmutableMultiDict + +class TestHoneypot(DoajTestCase): + + def setUp(self): + pass + + def test_01_valid_form_with_valid_honeypot(self): + valid_form = RegisterForm(registrationForm.create_valid_form_with_valid_honeypot()) + assert valid_form.is_bot() is False, "Test failed: The form should not be identified as a bot." + + def test_02_valid_form_with_invalid_honeypot_timer_exceeds(self): + valid_form = RegisterForm(registrationForm.create_valid_form_with_invalid_honeypot_timer_exceeds()) + assert valid_form.is_bot() is True, "Test failed: The form should be identified as a bot due to honeypot timer exceeding threshold." + + def test_03_valid_form_with_invalid_honeypot_email_not_empty(self): + valid_form = RegisterForm(registrationForm.create_valid_form_with_invalid_honeypot_email_not_empty()) + assert valid_form.is_bot() is True, "Test failed: The form should be identified as a bot due to honeypot email field not being empty." + + def test_04_invalid_form_with_valid_honeypot(self): + invalid_form = RegisterForm(registrationForm.create_invalid_form_with_valid_honeypot()) + assert invalid_form.is_bot() is False, "Test failed: The form should not be identified as a bot since honeypot is valid." + + def test_05_invalid_form_with_invalid_honeypot_timer_exceeds(self): + invalid_form = RegisterForm(registrationForm.create_invalid_form_with_invalid_honeypot_timer_exceeds()) + assert invalid_form.is_bot() is True, "Test failed: The form should be identified as a bot due to honeypot timer exceeding threshold." + + def test_06_invalid_form_with_invalid_honeypot_email_not_empty(self): + invalid_form = RegisterForm(registrationForm.create_invalid_form_with_invalid_honeypot_email_not_empty()) + assert invalid_form.is_bot() is True, "Test failed: The form should be identified as a bot due to honeypot email field not being empty." + + def test_07_valid_form_with_invalid_honeypot_both_fields(self): + valid_form = RegisterForm(registrationForm.create_valid_form_with_invalid_honeypot_both_fields()) + assert valid_form.is_bot() is True, "Test failed: The form should be identified as a bot due to both honeypot fields being invalid." + + def test_08_invalid_form_with_invalid_honeypot_both_fields(self): + invalid_form = RegisterForm(registrationForm.create_invalid_form_with_invalid_honeypot_both_fields()) + assert invalid_form.is_bot() is True, "Test failed: The form should be identified as a bot due to both honeypot fields being invalid." \ No newline at end of file diff --git a/portality/app.py b/portality/app.py index 0ad6e4f609..3eade6a17b 100644 --- a/portality/app.py +++ b/portality/app.py @@ -413,14 +413,6 @@ def api_directory(): ) return jsonify({'api_versions': vers}) - -# Make the reCAPTCHA key available to the js -# ~~-> ReCAPTCHA:ExternalService~~ -@app.route('/get_recaptcha_site_key') -def get_site_key(): - return app.config.get('RECAPTCHA_SITE_KEY', '') - - @app.errorhandler(400) def page_not_found(e): return render_template(templates.ERROR_400), 400 diff --git a/portality/settings.py b/portality/settings.py index 9d6e313e6b..cfeca1d51d 100644 --- a/portality/settings.py +++ b/portality/settings.py @@ -1342,15 +1342,6 @@ # assume it's a zombie, and ignore it HARVESTER_ZOMBIE_AGE = 604800 -####################################################### -# ReCAPTCHA configuration -# ~~->ReCAPTCHA:ExternalService - -#Recaptcha test keys, should be overridden in dev.cfg by the keys obtained from Google ReCaptcha v2 -RECAPTCHA_ENABLE = True -RECAPTCHA_SITE_KEY = '6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI' -RECAPTCHA_SECRET_KEY = "6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe" - ####################################################### # Preservation configuration # ~~->Preservation:Feature @@ -1554,3 +1545,7 @@ BGJOB_MANAGE_REDUNDANT_ACTIONS = [ 'read_news', 'journal_csv' ] + +################################################## +# Honeypot bot-trap settings for forms (now: only registration form) +HONEYPOT_TIMER_THRESHOLD = 5000; diff --git a/portality/static/js/honeypot.js b/portality/static/js/honeypot.js new file mode 100644 index 0000000000..bd972abdaf --- /dev/null +++ b/portality/static/js/honeypot.js @@ -0,0 +1,18 @@ +// ~~Honeypot:Feature~~ +doaj.honeypot = {} + +doaj.honeypot.init = function () { + console.log("init"); + doaj.honeypot.startTime = performance.now(); + $("#submitBtn").on("click", (event) => doaj.honeypot.handleRegistration(event)); +} + +doaj.honeypot.handleRegistration = function (event) { + event.preventDefault(); + const endTime = performance.now(); + const elapsedTime = endTime - doaj.honeypot.startTime; + // reset the timer + doaj.honeypot.startTime = performance.now(); + $("#hptimer").val(elapsedTime); + $("#registrationForm").submit(); +} \ No newline at end of file diff --git a/portality/static/js/recaptcha.js b/portality/static/js/recaptcha.js deleted file mode 100644 index 9b64fb388a..0000000000 --- a/portality/static/js/recaptcha.js +++ /dev/null @@ -1,21 +0,0 @@ -var onloadCallback = function() { - - $("#submitBtn").prop("disabled", true); - - var captchaCallback = function(param) { - $('#recaptcha_value').val(param); - $("#submitBtn").prop("disabled", false); - }; - - function ajax1() { - return $.get("/get_recaptcha_site_key"); - } - - $.when(ajax1()).done(function(key) { - grecaptcha.render('recaptcha_div', { - 'sitekey' : key, - 'callback' : captchaCallback, - }); - } - ); -}; \ No newline at end of file diff --git a/portality/templates-v2/_account/includes/_register_form.html b/portality/templates-v2/_account/includes/_register_form.html index 718fee8793..38fd018520 100644 --- a/portality/templates-v2/_account/includes/_register_form.html +++ b/portality/templates-v2/_account/includes/_register_form.html @@ -1,30 +1,31 @@ {% from "includes/_formhelpers.html" import render_field %} -
+ +