feat: added captcha validation in discussion thread/comment creation api (#37015)
This commit is contained in:
@@ -3,6 +3,10 @@ Discussion settings.
|
||||
"""
|
||||
from django.conf import settings
|
||||
|
||||
from openedx.core.djangoapps.waffle_utils import CourseWaffleFlag
|
||||
|
||||
WAFFLE_NAMESPACE = 'discussion'
|
||||
|
||||
# .. toggle_name: FEATURES['ENABLE_FORUM_DAILY_DIGEST']
|
||||
# .. toggle_implementation: DjangoSetting
|
||||
# .. toggle_default: False
|
||||
@@ -17,3 +21,13 @@ ENABLE_FORUM_DAILY_DIGEST = 'enable_forum_daily_digest'
|
||||
def is_forum_daily_digest_enabled():
|
||||
"""Returns whether forum notification features should be visible"""
|
||||
return settings.FEATURES.get('ENABLE_FORUM_DAILY_DIGEST', False)
|
||||
|
||||
# .. toggle_name: discussion.enable_captcha
|
||||
# .. toggle_implementation: CourseWaffleFlag
|
||||
# .. toggle_default: False
|
||||
# .. toggle_description: Waffle flag to enable account level preferences for notifications
|
||||
# .. toggle_use_cases: temporary, open_edx
|
||||
# .. toggle_creation_date: 2025-07-12
|
||||
# .. toggle_target_removal_date: 2025-07-29
|
||||
# .. toggle_warning: When the flag is ON, users will be able to see captcha for discussion.
|
||||
ENABLE_CAPTCHA_IN_DISCUSSION = CourseWaffleFlag(f'{WAFFLE_NAMESPACE}.enable_captcha', __name__)
|
||||
|
||||
@@ -128,7 +128,7 @@ from .utils import (
|
||||
get_usernames_from_search_string,
|
||||
set_attribute,
|
||||
is_posting_allowed,
|
||||
can_user_notify_all_learners
|
||||
can_user_notify_all_learners, is_captcha_enabled
|
||||
)
|
||||
|
||||
User = get_user_model()
|
||||
@@ -378,6 +378,11 @@ def get_course(request, course_key, check_tab=True):
|
||||
'is_notify_all_learners_enabled': can_user_notify_all_learners(
|
||||
course_key, user_roles, is_course_staff, is_course_admin
|
||||
),
|
||||
'captcha_settings': {
|
||||
'enabled': is_captcha_enabled(course_key),
|
||||
'site_key': settings.RECAPTCHA_SITE_KEY,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -214,7 +214,11 @@ class GetCourseTest(ForumsEnableMixin, UrlResetMixin, SharedModuleStoreTestCase)
|
||||
'edit_reasons': [{'code': 'test-edit-reason', 'label': 'Test Edit Reason'}],
|
||||
'post_close_reasons': [{'code': 'test-close-reason', 'label': 'Test Close Reason'}],
|
||||
'show_discussions': True,
|
||||
'is_notify_all_learners_enabled': False
|
||||
'is_notify_all_learners_enabled': False,
|
||||
'captcha_settings': {
|
||||
'enabled': False,
|
||||
'site_key': '',
|
||||
},
|
||||
}
|
||||
|
||||
@ddt.data(
|
||||
|
||||
@@ -557,7 +557,11 @@ class CourseViewTest(DiscussionAPIViewTestMixin, ModuleStoreTestCase):
|
||||
"edit_reasons": [{"code": "test-edit-reason", "label": "Test Edit Reason"}],
|
||||
"post_close_reasons": [{"code": "test-close-reason", "label": "Test Close Reason"}],
|
||||
'show_discussions': True,
|
||||
'is_notify_all_learners_enabled': False
|
||||
'is_notify_all_learners_enabled': False,
|
||||
'captcha_settings': {
|
||||
'enabled': False,
|
||||
'site_key': '',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -4,12 +4,17 @@ Utils for discussion API.
|
||||
from datetime import datetime
|
||||
from typing import Dict, List
|
||||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models.functions import Length
|
||||
from pytz import UTC
|
||||
|
||||
from common.djangoapps.student.roles import CourseInstructorRole, CourseStaffRole
|
||||
from openedx.core.djangoapps.django_comment_common.comment_client.thread import Thread
|
||||
|
||||
from lms.djangoapps.discussion.config.settings import ENABLE_CAPTCHA_IN_DISCUSSION
|
||||
from lms.djangoapps.discussion.django_comment_client.utils import has_discussion_privileges
|
||||
from openedx.core.djangoapps.notifications.config.waffle import ENABLE_NOTIFY_ALL_LEARNERS
|
||||
from openedx.core.djangoapps.discussions.models import DiscussionsConfiguration, PostingRestriction
|
||||
@@ -402,3 +407,39 @@ def can_user_notify_all_learners(course_key, user_roles, is_course_staff, is_cou
|
||||
])
|
||||
|
||||
return is_staff_or_instructor and ENABLE_NOTIFY_ALL_LEARNERS.is_enabled(course_key)
|
||||
|
||||
|
||||
def verify_recaptcha_token(token):
|
||||
"""
|
||||
Helper function to verify reCAPTCHA token
|
||||
"""
|
||||
verify_url = settings.RECAPTCHA_VERIFY_URL
|
||||
verify_data = {
|
||||
'secret': settings.RECAPTCHA_PRIVATE_KEY,
|
||||
'response': token,
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(verify_url, data=verify_data, timeout=10)
|
||||
result = response.json()
|
||||
return result.get('success', False)
|
||||
except: # pylint: disable=bare-except
|
||||
return False
|
||||
|
||||
|
||||
def is_captcha_enabled(course_id) -> bool:
|
||||
"""
|
||||
Check if reCAPTCHA is enabled for discussion posts in the given course.
|
||||
"""
|
||||
return bool(ENABLE_CAPTCHA_IN_DISCUSSION.is_enabled(course_id) and settings.RECAPTCHA_PRIVATE_KEY)
|
||||
|
||||
|
||||
def get_course_id_from_thread_id(thread_id: str) -> str:
|
||||
"""
|
||||
Get course id from thread id.
|
||||
"""
|
||||
thread = Thread(id=thread_id).retrieve(**{
|
||||
'with_responses': False,
|
||||
'mark_as_read': False
|
||||
})
|
||||
return thread["course_id"]
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"""
|
||||
Discussion API views
|
||||
"""
|
||||
|
||||
import logging
|
||||
import uuid
|
||||
|
||||
import edx_api_doc_tools as apidocs
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.exceptions import BadRequest, ValidationError
|
||||
from django.shortcuts import get_object_or_404
|
||||
@@ -20,6 +20,7 @@ from rest_framework.parsers import JSONParser
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.viewsets import ViewSet
|
||||
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
from common.djangoapps.util.file import store_uploaded_file
|
||||
@@ -79,10 +80,9 @@ from ..rest_api.serializers import (
|
||||
)
|
||||
from .utils import (
|
||||
create_blocks_params,
|
||||
create_topics_v3_structure,
|
||||
create_topics_v3_structure, is_captcha_enabled, verify_recaptcha_token, get_course_id_from_thread_id,
|
||||
)
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
User = get_user_model()
|
||||
@@ -664,7 +664,18 @@ class ThreadViewSet(DeveloperErrorViewMixin, ViewSet):
|
||||
Implements the POST method for the list endpoint as described in the
|
||||
class docstring.
|
||||
"""
|
||||
return Response(create_thread(request, request.data))
|
||||
if not request.data.get("course_id"):
|
||||
raise ValidationError({"course_id": ["This field is required."]})
|
||||
if is_captcha_enabled(CourseKey.from_string(request.data.get("course_id"))):
|
||||
captcha_token = request.data.get('captcha_token')
|
||||
if not captcha_token:
|
||||
raise ValidationError({'captcha_token': 'This field is required.'})
|
||||
|
||||
if not verify_recaptcha_token(captcha_token):
|
||||
return Response({'error': 'CAPTCHA verification failed.'}, status=400)
|
||||
data = request.data.copy()
|
||||
data.pop('captcha_token', None)
|
||||
return Response(create_thread(request, data))
|
||||
|
||||
def partial_update(self, request, thread_id):
|
||||
"""
|
||||
@@ -1019,6 +1030,18 @@ class CommentViewSet(DeveloperErrorViewMixin, ViewSet):
|
||||
Implements the POST method for the list endpoint as described in the
|
||||
class docstring.
|
||||
"""
|
||||
if not request.data.get("thread_id"):
|
||||
raise ValidationError({"thread_id": ["This field is required."]})
|
||||
course_id = get_course_id_from_thread_id(request.data["thread_id"])
|
||||
if is_captcha_enabled(CourseKey.from_string(course_id)):
|
||||
captcha_token = request.data.get('captcha_token')
|
||||
if not captcha_token:
|
||||
raise ValidationError({'captcha_token': 'This field is required.'})
|
||||
|
||||
if not verify_recaptcha_token(captcha_token):
|
||||
return Response({'error': 'CAPTCHA verification failed.'}, status=400)
|
||||
data = request.data.copy()
|
||||
data.pop('captcha_token', None)
|
||||
return Response(create_comment(request, request.data))
|
||||
|
||||
def destroy(self, request, comment_id):
|
||||
|
||||
@@ -3378,7 +3378,6 @@ INSTALLED_APPS = [
|
||||
"openedx_learning.apps.authoring.sections",
|
||||
]
|
||||
|
||||
|
||||
######################### CSRF #########################################
|
||||
|
||||
# Forwards-compatibility with Django 1.7
|
||||
@@ -5684,3 +5683,18 @@ USE_EXTRACTED_PROBLEM_BLOCK = False
|
||||
# .. toggle_creation_date: 2024-11-10
|
||||
# .. toggle_target_removal_date: 2025-06-01
|
||||
USE_EXTRACTED_VIDEO_BLOCK = False
|
||||
|
||||
# .. setting_name: RECAPTCHA_PRIVATE_KEY
|
||||
# .. setting_default: empty string
|
||||
# .. setting_description: Add recaptcha private key to use captcha feature in discussion app.
|
||||
RECAPTCHA_PRIVATE_KEY = ""
|
||||
|
||||
# .. setting_name: RECAPTCHA_VERIFY_URL
|
||||
# .. setting_default: empty string
|
||||
# .. setting_description: Add recaptcha verification api url to verify capthca tokens.
|
||||
RECAPTCHA_VERIFY_URL = ""
|
||||
|
||||
# .. setting_name: RECAPTCHA_SITE_KEY
|
||||
# .. setting_default: empty string
|
||||
# .. setting_description: Add recaptcha site key to use captcha feature in discussion MFE.
|
||||
RECAPTCHA_SITE_KEY = ""
|
||||
|
||||
Reference in New Issue
Block a user