diff --git a/README.rst b/README.rst index 7892c4c..b863b0d 100644 --- a/README.rst +++ b/README.rst @@ -176,6 +176,20 @@ Config settings # The day of the week that weekly notification subscriptions are sent ckanext.subscribe.weekly_notification_day = friday + *** reCAPTCHA implementation *** + Applying reCAPTCHA helps enhance the security of the dataset subscription form by preventing automated bots from submitting them. + + To integrate reCAPTCHA, you must first register your domain at https://www.google.com/recaptcha and obtain private and public keys. + These keys, along with the reCAPTCHA API URL, need to be configured in your setup:: + + ckanext.subscribe.recaptcha.privatekey = [Your_Private_Key] + ckanext.subscribe.recaptcha.publickey = [Your_Public_Key] + ckanext.subscribe.recaptcha.api_url = [Your_API_URL], e.g., https://www.google.com/recaptcha/api/siteverify + + Additionally, ensure that the `apply_recaptcha` configuration is set to true in order to enable reCAPTCHA:: + + ckanext.subscribe.apply_recaptcha = false + --------------- Troubleshooting diff --git a/ckanext/subscribe/action.py b/ckanext/subscribe/action.py index aaae082..c19fbb0 100644 --- a/ckanext/subscribe/action.py +++ b/ckanext/subscribe/action.py @@ -2,10 +2,11 @@ import logging import datetime - +import requests import ckan.plugins as p from ckan.logic import validate # put in toolkit? from ckan.lib.mailer import MailerException +import ckan.plugins.toolkit as tk from ckanext.subscribe.model import Subscription, Frequency from ckanext.subscribe import ( @@ -21,6 +22,28 @@ NotFound = p.toolkit.ObjectNotFound +def _verify_recaptcha(recaptcha_response): + secret_key = tk.config.get('ckanext.subscribe.recaptcha.privatekey', '') + if not secret_key: + log.error('reCAPTCHA secret key is not configured.') + return False + + payload = { + 'secret': secret_key, + 'response': recaptcha_response + } + + recaptcha_api_url = tk.config.get( + 'ckanext.subscribe.recaptcha.api_url', + 'https://www.google.com/recaptcha/api/siteverify' + ) + + r = requests.post(recaptcha_api_url, data=payload) + result = r.json() + + return result.get('success', False) + + @validate(schema.subscribe_schema) def subscribe_signup(context, data_dict): '''Signup to get notifications of email. Causes a email to be sent, @@ -38,6 +61,7 @@ def subscribe_signup(context, data_dict): :param skip_verification: Doesn't send email - instead it marks the subscription as verified. Can be used by sysadmins only. (optional, default=False) + :param g_recaptcha_response: Recaptcha response from the signup form :returns: the newly created subscription :rtype: dictionary @@ -45,6 +69,18 @@ def subscribe_signup(context, data_dict): ''' model = context['model'] + # Retrieve the configuration value to apply recaptcha + apply_recaptcha = tk.asbool(tk.config.get( + 'ckanext.subscribe.apply_recaptcha', True + )) + + if apply_recaptcha: + # Verify reCAPTCHA response + recaptcha_response = data_dict['g_recaptcha_response'] + if not _verify_recaptcha(recaptcha_response): + raise tk.ValidationError('Invalid reCAPTCHA. Please try again.') + log.info('reCAPTCHA verification passed.') + _check_access(u'subscribe_signup', context, data_dict) data = { diff --git a/ckanext/subscribe/controller.py b/ckanext/subscribe/controller.py index a13dd72..b8c0c1d 100644 --- a/ckanext/subscribe/controller.py +++ b/ckanext/subscribe/controller.py @@ -45,6 +45,7 @@ def signup(self): 'dataset_id': request.POST.get('dataset'), 'group_id': request.POST.get('group'), 'organization_id': request.POST.get('organization'), + 'g_recaptcha_response': request.POST.get('g-recaptcha-response'), } context = { u'model': model, diff --git a/ckanext/subscribe/helpers.py b/ckanext/subscribe/helpers.py new file mode 100644 index 0000000..cc1e7eb --- /dev/null +++ b/ckanext/subscribe/helpers.py @@ -0,0 +1,13 @@ +import ckan.plugins.toolkit as tk + + +def get_recaptcha_publickey(): + """Get reCaptcha public key. + """ + return tk.config.get('ckanext.subscribe.recaptcha.publickey') + +def apply_recaptcha(): + """Apply recaptcha""" + apply_recaptcha = tk.asbool( + tk.config.get('ckanext.subscribe.apply_recaptcha', True)) + return apply_recaptcha diff --git a/ckanext/subscribe/plugin.py b/ckanext/subscribe/plugin.py index b4f16c8..a64b989 100644 --- a/ckanext/subscribe/plugin.py +++ b/ckanext/subscribe/plugin.py @@ -1,13 +1,13 @@ # encoding: utf-8 import ckan.plugins as plugins -import ckan.plugins.toolkit as toolkit +import ckan.plugins.toolkit as tk from ckanext.subscribe import action from ckanext.subscribe import auth from ckanext.subscribe import model as subscribe_model from ckanext.subscribe.interfaces import ISubscribe - +import ckanext.subscribe.helpers as subscribe_helpers class SubscribePlugin(plugins.SingletonPlugin): plugins.implements(plugins.IConfigurer) @@ -15,13 +15,14 @@ class SubscribePlugin(plugins.SingletonPlugin): plugins.implements(plugins.IActions) plugins.implements(plugins.IAuthFunctions) plugins.implements(ISubscribe, inherit=True) + plugins.implements(plugins.ITemplateHelpers) # IConfigurer def update_config(self, config_): - toolkit.add_template_directory(config_, 'templates') - toolkit.add_public_directory(config_, 'public') - toolkit.add_resource('fanstatic', 'subscribe') + tk.add_template_directory(config_, 'templates') + tk.add_public_directory(config_, 'public') + tk.add_resource('fanstatic', 'subscribe') subscribe_model.setup() @@ -81,3 +82,13 @@ def get_auth_functions(self): 'subscribe_send_any_notifications': auth.subscribe_send_any_notifications, } + + # ITemplateHelpers + def get_helpers(self): + """Provide template helper functions + """ + + return { + 'get_recaptcha_publickey': subscribe_helpers.get_recaptcha_publickey, # noqa + 'apply_recaptcha': subscribe_helpers.apply_recaptcha, + } diff --git a/ckanext/subscribe/schema.py b/ckanext/subscribe/schema.py index 178b38b..6a4e6f2 100644 --- a/ckanext/subscribe/schema.py +++ b/ckanext/subscribe/schema.py @@ -14,7 +14,6 @@ ignore_missing = get_validator('ignore_missing') boolean_validator = get_validator('boolean_validator') - def one_package_or_group_or_org(key, data, errors, context): num_objects_specified = len(filter(None, [data[('dataset_id',)], data[('group_id',)], @@ -44,6 +43,7 @@ def subscribe_schema(): u'email': [email], u'frequency': [ignore_empty, frequency_name_to_int], u'skip_verification': [boolean_validator], + u'g_recaptcha_response': [ignore_empty], } diff --git a/ckanext/subscribe/templates/package/snippets/info.html b/ckanext/subscribe/templates/package/snippets/info.html index d3dcb52..e0355b2 100644 --- a/ckanext/subscribe/templates/package/snippets/info.html +++ b/ckanext/subscribe/templates/package/snippets/info.html @@ -23,6 +23,11 @@