diff --git a/wagtail/admin/compare.py b/wagtail/admin/compare.py
index 3c64f158f9..dc7a416e62 100644
--- a/wagtail/admin/compare.py
+++ b/wagtail/admin/compare.py
@@ -10,6 +10,11 @@ from django.utils.translation import gettext_lazy as _
from wagtail.core import blocks
+def text_from_html(val):
+ # Return the unescaped text content of an HTML string
+ return BeautifulSoup(force_str(val), 'html5lib').getText()
+
+
class FieldComparison:
is_field = True
is_child_relation = False
@@ -52,15 +57,18 @@ class TextFieldComparison(FieldComparison):
class RichTextFieldComparison(TextFieldComparison):
def htmldiff(self):
return diff_text(
- BeautifulSoup(force_str(self.val_a), 'html5lib').getText(),
- BeautifulSoup(force_str(self.val_b), 'html5lib').getText()
+ text_from_html(self.val_a),
+ text_from_html(self.val_b)
).to_html()
def get_comparison_class_for_block(block):
if hasattr(block, 'get_comparison_class'):
return block.get_comparison_class()
- elif isinstance(block, blocks.CharBlock):
+ elif isinstance(block, (blocks.CharBlock, blocks.TextBlock)):
+ return CharBlockComparison
+ elif isinstance(block, blocks.RawHTMLBlock):
+ # Compare raw HTML blocks as if they were plain text, so that tags are shown explicitly
return CharBlockComparison
elif isinstance(block, blocks.RichTextBlock):
return RichTextBlockComparison
@@ -89,7 +97,19 @@ class BlockComparison:
return self.val_a != self.val_b
def htmlvalue(self, val):
- return self.block.render_basic(val)
+ """
+ Return an HTML representation of this block that is safe to be included
+ in comparison views
+ """
+ return escape(text_from_html(self.block.render_basic(val)))
+
+ def htmldiff(self):
+ html_val_a = self.block.render_basic(self.val_a)
+ html_val_b = self.block.render_basic(self.val_b)
+ return diff_text(
+ text_from_html(html_val_a),
+ text_from_html(html_val_b)
+ ).to_html()
class CharBlockComparison(BlockComparison):
@@ -99,13 +119,12 @@ class CharBlockComparison(BlockComparison):
force_str(self.val_b)
).to_html()
+ def htmlvalue(self, val):
+ return escape(val)
+
class RichTextBlockComparison(BlockComparison):
- def htmldiff(self):
- return diff_text(
- BeautifulSoup(force_str(self.val_a), 'html5lib').getText(),
- BeautifulSoup(force_str(self.val_b), 'html5lib').getText()
- ).to_html()
+ pass
class StructBlockComparison(BlockComparison):
@@ -219,8 +238,8 @@ class StreamFieldComparison(FieldComparison):
else:
# Fall back to diffing the HTML representation
return diff_text(
- BeautifulSoup(force_str(self.val_a), 'html5lib').getText(),
- BeautifulSoup(force_str(self.val_b), 'html5lib').getText()
+ text_from_html(self.val_a),
+ text_from_html(self.val_b)
).to_html()
diff --git a/wagtail/admin/tests/test_compare.py b/wagtail/admin/tests/test_compare.py
index 262f4ba91d..2348ba11f4 100644
--- a/wagtail/admin/tests/test_compare.py
+++ b/wagtail/admin/tests/test_compare.py
@@ -247,36 +247,151 @@ class TestStreamFieldComparison(TestCase):
self.assertIsInstance(comparison.htmldiff(), SafeString)
self.assertTrue(comparison.has_changed())
- def test_htmldiff_escapes_value(self):
+ def test_htmldiff_escapes_value_on_change(self):
field = StreamPage._meta.get_field('body')
comparison = self.comparison_class(
field,
StreamPage(body=StreamValue(field.stream_block, [
- ('text', "Original content", '1'),
+ ('text', "I really like originalish content", '1'),
])),
StreamPage(body=StreamValue(field.stream_block, [
- ('text', '', '1'),
+ ('text', 'I really like evil code ', '1'),
])),
)
- self.assertEqual(comparison.htmldiff(), '
Original content<script type="text/javascript">doSomethingBad();</script>
')
+ self.assertEqual(comparison.htmldiff(), 'I <b>really</b> like original<i>ish</i> contentevil code <script type="text/javascript">doSomethingBad();</script>
')
self.assertIsInstance(comparison.htmldiff(), SafeString)
- def test_htmldiff_escapes_value_richtext(self):
+ def test_htmldiff_escapes_value_on_addition(self):
field = StreamPage._meta.get_field('body')
comparison = self.comparison_class(
field,
StreamPage(body=StreamValue(field.stream_block, [
- ('rich_text', "Original content", '1'),
+ ('text', "Original and unchanged content", '1'),
])),
StreamPage(body=StreamValue(field.stream_block, [
- ('rich_text', '', '1'),
+ ('text', "Original and unchanged content", '1'),
+ ('text', '', '2'),
])),
)
- self.assertEqual(comparison.htmldiff(), 'Original contentdoSomethingBad();
')
+ self.assertEqual(comparison.htmldiff(), 'Original <em>and unchanged</em> content
\n<script type="text/javascript">doSomethingBad();</script>
')
+ self.assertIsInstance(comparison.htmldiff(), SafeString)
+
+ def test_htmldiff_escapes_value_on_deletion(self):
+ field = StreamPage._meta.get_field('body')
+
+ comparison = self.comparison_class(
+ field,
+ StreamPage(body=StreamValue(field.stream_block, [
+ ('text', "Original and unchanged content", '1'),
+ ('text', '', '2'),
+ ])),
+ StreamPage(body=StreamValue(field.stream_block, [
+ ('text', "Original and unchanged content", '1'),
+ ])),
+ )
+
+ self.assertEqual(comparison.htmldiff(), 'Original <em>and unchanged</em> content
\n<script type="text/javascript">doSomethingBad();</script>
')
+ self.assertIsInstance(comparison.htmldiff(), SafeString)
+
+ def test_htmldiff_richtext_strips_tags_on_change(self):
+ field = StreamPage._meta.get_field('body')
+
+ comparison = self.comparison_class(
+ field,
+ StreamPage(body=StreamValue(field.stream_block, [
+ ('rich_text', "I really like Wagtail <3", '1'),
+ ])),
+ StreamPage(body=StreamValue(field.stream_block, [
+ ('rich_text', 'I really like evil code >_< ', '1'),
+ ])),
+ )
+
+ self.assertEqual(comparison.htmldiff(), 'I really like Wagtail <3evil code >_< doSomethingBad();
')
+ self.assertIsInstance(comparison.htmldiff(), SafeString)
+
+ def test_htmldiff_richtext_strips_tags_on_addition(self):
+ field = StreamPage._meta.get_field('body')
+
+ comparison = self.comparison_class(
+ field,
+ StreamPage(body=StreamValue(field.stream_block, [
+ ('rich_text', "Original and unchanged content", '1'),
+ ])),
+ StreamPage(body=StreamValue(field.stream_block, [
+ ('rich_text', "Original and unchanged content", '1'),
+ ('rich_text', 'I really like evil code >_< ', '2'),
+ ])),
+ )
+
+ self.assertEqual(comparison.htmldiff(), 'Original and unchanged content
\nI really like evil code >_< doSomethingBad();
')
+ self.assertIsInstance(comparison.htmldiff(), SafeString)
+
+ def test_htmldiff_richtext_strips_tags_on_deletion(self):
+ field = StreamPage._meta.get_field('body')
+
+ comparison = self.comparison_class(
+ field,
+ StreamPage(body=StreamValue(field.stream_block, [
+ ('rich_text', "Original and unchanged content", '1'),
+ ('rich_text', 'I really like evil code >_< ', '2'),
+ ])),
+ StreamPage(body=StreamValue(field.stream_block, [
+ ('rich_text', "Original and unchanged content", '1'),
+ ])),
+ )
+
+ self.assertEqual(comparison.htmldiff(), 'Original and unchanged content
\nI really like evil code >_< doSomethingBad();
')
+ self.assertIsInstance(comparison.htmldiff(), SafeString)
+
+ def test_htmldiff_raw_html_escapes_value_on_change(self):
+ field = StreamPage._meta.get_field('body')
+
+ comparison = self.comparison_class(
+ field,
+ StreamPage(body=StreamValue(field.stream_block, [
+ ('raw_html', "Originalish content", '1'),
+ ])),
+ StreamPage(body=StreamValue(field.stream_block, [
+ ('raw_html', '', '1'),
+ ])),
+ )
+ self.assertEqual(comparison.htmldiff(), 'Original<i>ish</i> content<script type="text/javascript">doSomethingBad();</script>
')
+ self.assertIsInstance(comparison.htmldiff(), SafeString)
+
+ def test_htmldiff_raw_html_escapes_value_on_addition(self):
+ field = StreamPage._meta.get_field('body')
+
+ comparison = self.comparison_class(
+ field,
+ StreamPage(body=StreamValue(field.stream_block, [
+ ('raw_html', "Original and unchanged content", '1'),
+ ])),
+ StreamPage(body=StreamValue(field.stream_block, [
+ ('raw_html', "Original and unchanged content", '1'),
+ ('raw_html', '', '2'),
+ ])),
+ )
+ self.assertEqual(comparison.htmldiff(), 'Original <em>and unchanged</em> content
\n<script type="text/javascript">doSomethingBad();</script>
')
+ self.assertIsInstance(comparison.htmldiff(), SafeString)
+
+ def test_htmldiff_raw_html_escapes_value_on_deletion(self):
+ field = StreamPage._meta.get_field('body')
+
+ comparison = self.comparison_class(
+ field,
+ StreamPage(body=StreamValue(field.stream_block, [
+ ('raw_html', "Original and unchanged content", '1'),
+ ('raw_html', '', '2'),
+ ])),
+ StreamPage(body=StreamValue(field.stream_block, [
+ ('raw_html', "Original and unchanged content", '1'),
+ ])),
+ )
+ self.assertEqual(comparison.htmldiff(), 'Original <em>and unchanged</em> content
\n<script type="text/javascript">doSomethingBad();</script>
')
self.assertIsInstance(comparison.htmldiff(), SafeString)
def test_compare_structblock(self):
diff --git a/wagtail/tests/testapp/migrations/0048_rawhtmlblock.py b/wagtail/tests/testapp/migrations/0048_rawhtmlblock.py
new file mode 100644
index 0000000000..19c901dccf
--- /dev/null
+++ b/wagtail/tests/testapp/migrations/0048_rawhtmlblock.py
@@ -0,0 +1,21 @@
+# Generated by Django 3.0.4 on 2020-04-06 09:46
+
+from django.db import migrations
+import wagtail.core.blocks
+import wagtail.core.fields
+import wagtail.tests.testapp.models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('tests', '0047_restaurant_tags'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='streampage',
+ name='body',
+ field=wagtail.core.fields.StreamField([('text', wagtail.core.blocks.CharBlock()), ('rich_text', wagtail.core.blocks.RichTextBlock()), ('image', wagtail.tests.testapp.models.ExtendedImageChooserBlock()), ('product', wagtail.core.blocks.StructBlock([('name', wagtail.core.blocks.CharBlock()), ('price', wagtail.core.blocks.CharBlock())])), ('raw_html', wagtail.core.blocks.RawHTMLBlock())]),
+ ),
+ ]
diff --git a/wagtail/tests/testapp/models.py b/wagtail/tests/testapp/models.py
index 3938ed1627..586672d21b 100644
--- a/wagtail/tests/testapp/models.py
+++ b/wagtail/tests/testapp/models.py
@@ -29,7 +29,7 @@ from wagtail.contrib.forms.models import (
from wagtail.contrib.settings.models import BaseSetting, register_setting
from wagtail.contrib.sitemaps import Sitemap
from wagtail.contrib.table_block.blocks import TableBlock
-from wagtail.core.blocks import CharBlock, RichTextBlock, StructBlock
+from wagtail.core.blocks import CharBlock, RawHTMLBlock, RichTextBlock, StructBlock
from wagtail.core.fields import RichTextField, StreamField
from wagtail.core.models import Orderable, Page, PageManager, PageQuerySet
from wagtail.documents.edit_handlers import DocumentChooserPanel
@@ -972,6 +972,7 @@ class StreamPage(Page):
('name', CharBlock()),
('price', CharBlock()),
])),
+ ('raw_html', RawHTMLBlock()),
])
api_fields = ('body',)