markdown: Switch to directly URL-escaping CSS URLs.

soupsieve is a heavy-weight dependency, and Tornado pulls it in by way
of markdown rendering; since we are only using it for a very simple
process, perform that manually.

Per CSS spec[^1]:

> In quoted <string> url()s, only newlines and the character used to
> quote the string need to be escaped.

[^1]: https://drafts.csswg.org/css-values/#urls
This commit is contained in:
Alex Vandiver 2024-04-15 21:37:58 +00:00 committed by Tim Abbott
parent 1424a2e748
commit 693b959656
5 changed files with 16 additions and 21 deletions

View File

@ -186,9 +186,6 @@ django-cte
# SCIM integration
django-scim2
# CSS manipulation
soupsieve
# Circuit-breaking for outgoing services
circuitbreaker

View File

@ -2894,9 +2894,7 @@ sortedcontainers==2.4.0 \
soupsieve==2.5 \
--hash=sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690 \
--hash=sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7
# via
# -r requirements/common.in
# beautifulsoup4
# via beautifulsoup4
sphinx==7.2.6 \
--hash=sha256:1e09160a40b956dc623c910118fa636da93bd3ca0b9876a7b3df90f07d691560 \
--hash=sha256:9a5160e1ea90688d5963ba09a2dcd8bdd526620edbb65c328728f1b2228d5ab5

View File

@ -2240,9 +2240,7 @@ social-auth-core[azuread,openidconnect,saml]==4.5.3 \
soupsieve==2.5 \
--hash=sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690 \
--hash=sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7
# via
# -r requirements/common.in
# beautifulsoup4
# via beautifulsoup4
sqlalchemy==1.4.52 \
--hash=sha256:1296f2cdd6db09b98ceb3c93025f0da4835303b8ac46c15c2136e27ee4d18d94 \
--hash=sha256:1e135fff2e84103bc15c07edd8569612ce317d64bdb391f49ce57124a73f45c5 \

View File

@ -47,7 +47,6 @@ import uri_template
from django.conf import settings
from markdown.blockparser import BlockParser
from markdown.extensions import codehilite, nl2br, sane_lists, tables
from soupsieve import escape as css_escape
from tlds import tld_set
from typing_extensions import Self, TypeAlias, override
@ -690,7 +689,12 @@ class InlineInterestingLinkProcessor(markdown.treeprocessors.Treeprocessor):
img_link = get_camo_url(extracted_data.image)
img = SubElement(container, "a")
img.set("style", "background-image: url(" + css_escape(img_link) + ")")
img.set(
"style",
'background-image: url("'
+ img_link.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\a ")
+ '")',
)
img.set("href", link)
img.set("class", "message_embed_image")

View File

@ -534,7 +534,7 @@ class PreviewTestCase(ZulipTestCase):
@override_settings(CAMO_URI="")
def test_inline_url_embed_preview(self) -> None:
with_preview = '<p><a href="http://test.org/">http://test.org/</a></p>\n<div class="message_embed"><a class="message_embed_image" href="http://test.org/" style="background-image: url(http\\:\\/\\/ia\\.media-imdb\\.com\\/images\\/rock\\.jpg)"></a><div class="data-container"><div class="message_embed_title"><a href="http://test.org/" title="The Rock">The Rock</a></div><div class="message_embed_description">Description text</div></div></div>'
with_preview = '<p><a href="http://test.org/">http://test.org/</a></p>\n<div class="message_embed"><a class="message_embed_image" href="http://test.org/" style="background-image: url(&quot;http://ia.media-imdb.com/images/rock.jpg&quot;)"></a><div class="data-container"><div class="message_embed_title"><a href="http://test.org/" title="The Rock">The Rock</a></div><div class="message_embed_description">Description text</div></div></div>'
without_preview = '<p><a href="http://test.org/">http://test.org/</a></p>'
msg = self._send_message_with_test_org_url(sender=self.example_user("hamlet"))
self.assertEqual(msg.rendered_content, with_preview)
@ -549,13 +549,11 @@ class PreviewTestCase(ZulipTestCase):
self.assertEqual(msg.rendered_content, without_preview)
def test_inline_url_embed_preview_with_camo(self) -> None:
camo_url = re.sub(
r"([^\w-])", r"\\\1", get_camo_url("http://ia.media-imdb.com/images/rock.jpg")
)
camo_url = get_camo_url("http://ia.media-imdb.com/images/rock.jpg")
with_preview = (
'<p><a href="http://test.org/">http://test.org/</a></p>\n<div class="message_embed"><a class="message_embed_image" href="http://test.org/" style="background-image: url('
'<p><a href="http://test.org/">http://test.org/</a></p>\n<div class="message_embed"><a class="message_embed_image" href="http://test.org/" style="background-image: url(&quot;'
+ camo_url
+ ')"></a><div class="data-container"><div class="message_embed_title"><a href="http://test.org/" title="The Rock">The Rock</a></div><div class="message_embed_description">Description text</div></div></div>'
+ '&quot;)"></a><div class="data-container"><div class="message_embed_title"><a href="http://test.org/" title="The Rock">The Rock</a></div><div class="message_embed_description">Description text</div></div></div>'
)
msg = self._send_message_with_test_org_url(sender=self.example_user("hamlet"))
self.assertEqual(msg.rendered_content, with_preview)
@ -575,7 +573,7 @@ class PreviewTestCase(ZulipTestCase):
event = patched.call_args[0][1]
# Swap the URL out for one with characters that need CSS escaping
html = re.sub(r"rock\.jpg", "rock).jpg", self.open_graph_html)
html = re.sub(r"rock\.jpg", r"rock.jpg\\", self.open_graph_html)
self.create_mock_response(url, body=html)
with self.settings(TEST_SUITE=False):
with self.assertLogs(level="INFO") as info_logs:
@ -590,7 +588,7 @@ class PreviewTestCase(ZulipTestCase):
'<p><a href="http://test.org/">http://test.org/</a></p>\n'
'<div class="message_embed"><a class="message_embed_image" href="http://test.org/"'
' style="background-image:'
' url(http\\:\\/\\/ia\\.media-imdb\\.com\\/images\\/rock\\)\\.jpg)"></a><div'
' url(&quot;http://ia.media-imdb.com/images/rock.jpg\\\\&quot;)"></a><div'
' class="data-container"><div class="message_embed_title"><a href="http://test.org/"'
' title="The Rock">The Rock</a></div><div class="message_embed_description">Description'
" text</div></div></div>"
@ -614,7 +612,7 @@ class PreviewTestCase(ZulipTestCase):
@override_settings(CAMO_URI="")
def test_inline_url_embed_preview_with_relative_image_url(self) -> None:
with_preview_relative = '<p><a href="http://test.org/">http://test.org/</a></p>\n<div class="message_embed"><a class="message_embed_image" href="http://test.org/" style="background-image: url(http\\:\\/\\/test\\.org\\/images\\/rock\\.jpg)"></a><div class="data-container"><div class="message_embed_title"><a href="http://test.org/" title="The Rock">The Rock</a></div><div class="message_embed_description">Description text</div></div></div>'
with_preview_relative = '<p><a href="http://test.org/">http://test.org/</a></p>\n<div class="message_embed"><a class="message_embed_image" href="http://test.org/" style="background-image: url(&quot;http://test.org/images/rock.jpg&quot;)"></a><div class="data-container"><div class="message_embed_title"><a href="http://test.org/" title="The Rock">The Rock</a></div><div class="message_embed_description">Description text</div></div></div>'
# Try case where the Open Graph image is a relative URL.
msg = self._send_message_with_test_org_url(
sender=self.example_user("prospero"), relative_url=True
@ -832,7 +830,7 @@ class PreviewTestCase(ZulipTestCase):
assert msg.rendered_content is not None
self.assertIn(cached_data.title, msg.rendered_content)
assert cached_data.image is not None
self.assertIn(re.sub(r"([^\w-])", r"\\\1", cached_data.image), msg.rendered_content)
self.assertIn(cached_data.image, msg.rendered_content)
@responses.activate
@override_settings(INLINE_URL_EMBED_PREVIEW=True)