From 05d4a2f731b2e0aaa7a114448b0d6f1194478143 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Fri, 21 Jan 2022 11:02:20 +0100
Subject: [PATCH] misc: linkify URLs in rich text comments (#60912)

---
 tests/form_pages/test_all.py | 8 ++++++++
 tests/test_widgets.py        | 8 ++++----
 wcs/qommon/form.py           | 9 ++++++---
 3 files changed, 18 insertions(+), 7 deletions(-)

diff --git a/tests/form_pages/test_all.py b/tests/form_pages/test_all.py
index fb4c470ca..a892b96e0 100644
--- a/tests/form_pages/test_all.py
+++ b/tests/form_pages/test_all.py
@@ -8896,3 +8896,11 @@ def test_rich_commentable_action(pub):
     resp.form['comment'] = '<p>   </p>'  # left ~empty
     resp = resp.form.submit('button_x1')
     assert resp.pyquery('.error').text() == 'required field'
+
+    # url to links
+    resp.form['comment'] = '<p>Here is the address: https://example.net</p>'
+    resp = resp.form.submit('button_x1').follow()
+    assert (
+        '<p>Here is the address: <a href="https://example.net" rel="nofollow">https://example.net</a></p>'
+        in resp.text
+    )
diff --git a/tests/test_widgets.py b/tests/test_widgets.py
index d973337d7..ec492a358 100644
--- a/tests/test_widgets.py
+++ b/tests/test_widgets.py
@@ -428,7 +428,7 @@ def test_wysiwygwidget():
     widget = WysiwygTextWidget('test')
     mock_form_submission(req, widget, {'test': '<a href="#">a</a>'})
     assert not widget.has_error()
-    assert widget.parse() == '<a href="#">a</a>'
+    assert widget.parse() == '<a href="#" rel="nofollow">a</a>'
 
     widget = WysiwygTextWidget('test')
     mock_form_submission(req, widget, {'test': '<a href="javascript:alert()">a</a>'})
@@ -463,7 +463,7 @@ def test_wysiwygwidget():
     assert not widget.has_error()
     assert (
         widget.parse()
-        == '<a href="{% if 1 > 2 %}héllo{% endif %}">{% if 2 > 1 %}{{plop|date:"Y"}}{% endif %}</a>'
+        == '<a href="{% if 1 > 2 %}héllo{% endif %}" rel="nofollow">{% if 2 > 1 %}{{plop|date:"Y"}}{% endif %}</a>'
     )
 
     # make sure it is kept intact even after ckeditor escaped characters
@@ -472,13 +472,13 @@ def test_wysiwygwidget():
         req,
         widget,
         {
-            'test': '<a href="{% if 1 &gt; 2 %}héllo{% endif %}">{% if 2 &gt; 1 %}{{plop|date:&quot;Y&quot;}}{% endif %}</a>'
+            'test': '<a href="{% if 1 &gt; 2 %}héllo{% endif %}" rel="nofollow">{% if 2 &gt; 1 %}{{plop|date:&quot;Y&quot;}}{% endif %}</a>'
         },
     )
     assert not widget.has_error()
     assert (
         widget.parse()
-        == '<a href="{% if 1 > 2 %}héllo{% endif %}">{% if 2 > 1 %}{{plop|date:"Y"}}{% endif %}</a>'
+        == '<a href="{% if 1 > 2 %}héllo{% endif %}" rel="nofollow">{% if 2 > 1 %}{{plop|date:"Y"}}{% endif %}</a>'
     )
 
 
diff --git a/wcs/qommon/form.py b/wcs/qommon/form.py
index 1effc5014..4296831b3 100644
--- a/wcs/qommon/form.py
+++ b/wcs/qommon/form.py
@@ -30,16 +30,18 @@ import re
 import sys
 import tempfile
 import time
+from functools import partial
 
 try:
     from PIL import Image
 except ImportError:
     Image = None
 
-import bleach
 import dns
 import dns.exception
 import dns.resolver
+from bleach import Cleaner
+from bleach.linkifier import LinkifyFilter
 
 try:
     import magic
@@ -2265,14 +2267,15 @@ class WysiwygTextWidget(TextWidget):
     def _parse(self, request):
         TextWidget._parse(self, request, use_validation_function=False)
         if self.value:
-            self.value = bleach.clean(
-                self.value,
+            cleaner = Cleaner(
                 tags=self.ALL_TAGS,
                 attributes=self.ALL_ATTRS,
                 styles=self.ALL_STYLES,
                 strip=True,
                 strip_comments=False,
+                filters=[partial(LinkifyFilter, skip_tags=['pre'], parse_email=False)],
             )
+            self.value = cleaner.clean(self.value)
             if self.value.startswith('<br />'):
                 self.value = self.value[6:]
             if self.value.endswith('<br />'):
-- 
2.34.1

