diff --git a/news/274.feature b/news/274.feature
new file mode 100644
index 00000000..e19df38e
--- /dev/null
+++ b/news/274.feature
@@ -0,0 +1 @@
+Implement content filtering for comments with configurable words @rohnsha0
\ No newline at end of file
diff --git a/src/plone/app/discussion/browser/comments.py b/src/plone/app/discussion/browser/comments.py
index e7f82293..7da0a565 100644
--- a/src/plone/app/discussion/browser/comments.py
+++ b/src/plone/app/discussion/browser/comments.py
@@ -1,5 +1,7 @@
from AccessControl import getSecurityManager
from AccessControl import Unauthorized
+from AccessControl.SecurityManagement import newSecurityManager
+from AccessControl.users import SimpleUser
from Acquisition import aq_inner
from DateTime import DateTime
from plone.app.discussion import _
@@ -260,6 +262,28 @@ def handleComment(self, action):
)
captcha.validate(data["captcha"])
+ from plone.app.discussion.filter import get_content_filter
+
+ content_filter = get_content_filter(context=self.context, request=self.request)
+
+ if content_filter.is_enabled():
+ filter_result = content_filter.check_content(data.get("text", ""))
+
+ if filter_result["filtered"]:
+ action = filter_result["action"]
+
+ if action == "reject":
+ # Reject the comment immediately
+ message = content_filter.get_rejection_message(
+ filter_result["matches"]
+ )
+ IStatusMessage(self.request).add(message, type="error")
+ return
+ else:
+ # Store action for post-creation handling
+ data["_content_filter_action"] = action
+ data["_content_filter_matches"] = filter_result["matches"]
+
# Create comment
comment = self.create_comment(data)
@@ -274,6 +298,19 @@ def handleComment(self, action):
# Add a comment to the conversation
comment_id = conversation.addComment(comment)
+ # Get the actual comment object from the conversation
+ # This is important because the workflow operations need the persistent object
+ comment = conversation.get(comment_id)
+
+ # Handle content filtering actions post-creation
+ filter_action = data.get("_content_filter_action")
+
+ if filter_action and self._handle_content_filter_action(
+ comment, filter_action, content_filter
+ ):
+ self.request.response.redirect(self.action)
+ return
+
# Redirect after form submit:
# If a user posts a comment and moderation is enabled, a message is
# shown to the user that his/her comment awaits moderation. If the user
@@ -281,11 +318,8 @@ def handleComment(self, action):
# to the comment.
can_review = getSecurityManager().checkPermission("Review comments", context)
workflowTool = getToolByName(context, "portal_workflow")
- comment_review_state = workflowTool.getInfoFor(
- comment,
- "review_state",
- None,
- )
+ comment_review_state = workflowTool.getInfoFor(comment, "review_state", None)
+
if comment_review_state == "pending" and not can_review:
# Show info message when comment moderation is enabled
IStatusMessage(self.context.REQUEST).addStatusMessage(
@@ -302,6 +336,63 @@ def handleCancel(self, action):
# a cancel button that is handled by a jQuery method.
pass # pragma: no cover
+ def _handle_content_filter_action(self, comment, action, content_filter):
+ """Handle content filter actions (spam/moderate) for a comment."""
+ context = aq_inner(self.context)
+ workflowTool = getToolByName(context, "portal_workflow")
+
+ if action == "spam":
+ try:
+ # Check if workflow supports spam state
+ comment_workflow = workflowTool.getWorkflowsFor(comment)
+ if comment_workflow and "spam" in comment_workflow[0].states:
+ # Use Zope security manager for privileged operations
+ # Store current security manager
+ old_security_manager = getSecurityManager()
+
+ try:
+ # Create a temporary security manager with Manager role
+ user = SimpleUser("system", "", ["Manager"], [])
+ user = user.__of__(self.context.acl_users)
+ newSecurityManager(None, user)
+
+ # Perform the workflow action with elevated privileges
+ workflowTool.doActionFor(comment, "mark_as_spam")
+ comment.reindexObject()
+ finally:
+ # Restore original security manager
+ newSecurityManager(None, old_security_manager.getUser())
+
+ message = _(
+ "comment_marked_spam_by_filter",
+ default="Your comment has been marked as spam due to filtered content.",
+ )
+ IStatusMessage(self.request).addStatusMessage(
+ message, type="warning"
+ )
+ return True
+ except Exception:
+ # Fallback to moderation if spam action fails
+ action = "moderate"
+
+ if action == "moderate":
+ try:
+ current_state = workflowTool.getInfoFor(comment, "review_state", None)
+ if current_state != "pending":
+ transitions = workflowTool.getTransitionsFor(comment)
+ available_transitions = [t["id"] for t in transitions]
+ if "recall" in available_transitions:
+ workflowTool.doActionFor(comment, "recall")
+ comment.reindexObject()
+
+ message = content_filter.get_moderation_message()
+ IStatusMessage(self.request).addStatusMessage(message, type="info")
+ return True
+ except Exception:
+ pass
+
+ return False
+
class CommentsViewlet(ViewletBase):
form = CommentForm
diff --git a/src/plone/app/discussion/browser/controlpanel.py b/src/plone/app/discussion/browser/controlpanel.py
index bb01a60b..cf61f74e 100644
--- a/src/plone/app/discussion/browser/controlpanel.py
+++ b/src/plone/app/discussion/browser/controlpanel.py
@@ -52,6 +52,9 @@ def updateFields(self):
self.fields["user_notification_enabled"].widgetFactory = (
SingleCheckBoxFieldWidget
)
+ self.fields["content_filter_enabled"].widgetFactory = SingleCheckBoxFieldWidget
+ self.fields["filter_case_sensitive"].widgetFactory = SingleCheckBoxFieldWidget
+ self.fields["filter_whole_words_only"].widgetFactory = SingleCheckBoxFieldWidget
def updateWidgets(self):
try:
@@ -70,6 +73,9 @@ def updateWidgets(self):
self.widgets["user_notification_enabled"].label = _(
"User Email Notification",
)
+ self.widgets["content_filter_enabled"].label = _(
+ "Content Filtering",
+ )
@button.buttonAndHandler(_("Save"), name=None)
def handleSave(self, action):
diff --git a/src/plone/app/discussion/configure.zcml b/src/plone/app/discussion/configure.zcml
index 50a6ccd5..91130fdd 100644
--- a/src/plone/app/discussion/configure.zcml
+++ b/src/plone/app/discussion/configure.zcml
@@ -112,6 +112,13 @@
component=".vocabularies.text_transform_vocabulary"
/>
+
+
+
False
False
+ False
+
+ reject
+ False
+ True
diff --git a/src/plone/app/discussion/vocabularies.py b/src/plone/app/discussion/vocabularies.py
index 3d28ee49..be66c377 100644
--- a/src/plone/app/discussion/vocabularies.py
+++ b/src/plone/app/discussion/vocabularies.py
@@ -69,6 +69,33 @@ def captcha_vocabulary(context):
return SimpleVocabulary(terms)
+def filter_action_vocabulary(context):
+ """Vocabulary with all available content filter actions."""
+ terms = []
+ terms.append(
+ SimpleTerm(
+ value="reject",
+ token="reject",
+ title=_("filter_action_reject", default="Reject"),
+ )
+ )
+ terms.append(
+ SimpleTerm(
+ value="moderate",
+ token="moderate",
+ title=_("filter_action_moderate", default="Send to moderation"),
+ )
+ )
+ terms.append(
+ SimpleTerm(
+ value="spam",
+ token="spam",
+ title=_("filter_action_spam", default="Mark as spam"),
+ )
+ )
+ return SimpleVocabulary(terms)
+
+
def text_transform_vocabulary(context):
"""Vocabulary with all available portal_transform transformations."""
terms = []