Skip to content

Commit

Permalink
prevent image_srcset from breaking when srcset config is missing, add…
Browse files Browse the repository at this point in the history
… tests
  • Loading branch information
MrTango committed Apr 20, 2022

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent b190b71 commit e700516
Showing 2 changed files with 228 additions and 17 deletions.
47 changes: 30 additions & 17 deletions plone/outputfilters/filters/image_srcset.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import logging
import re

from bs4 import BeautifulSoup
@@ -8,6 +9,8 @@
from zope.component import getUtility
from zope.interface import implementer

logger = logging.getLogger("plone.outputfilter.image_srcset")


@implementer(IFilter)
class ImageSrcsetFilter(object):
@@ -47,10 +50,15 @@ def __init__(self, context=None, request=None):
self.context = context
self.request = request

@property
def image_srcsets(self):
registry = getUtility(IRegistry)
settings = registry.forInterface(IImagingSchema, prefix="plone", check=False)
return settings.image_srcsets

def __call__(self, data):
data = re.sub(r"<([^<>\s]+?)\s*/>", self._shorttag_replace, data)
soup = BeautifulSoup(safe_nativestring(data), "html.parser")
self.image_srcsets = self.image_srcsets()

for elem in soup.find_all("img"):
srcset_name = elem.attrs.get("data-srcset", "")
@@ -59,31 +67,36 @@ def __call__(self, data):
elem.replace_with(self.convert_to_srcset(srcset_name, elem, soup))
return str(soup)

def image_srcsets(self):
registry = getUtility(IRegistry)
settings = registry.forInterface(IImagingSchema, prefix="plone", check=False)
return settings.image_srcsets

def convert_to_srcset(self, srcset_name, elem, soup):
"""Converts the element to a srcset definition
"""
"""Converts the element to a srcset definition"""
srcset_config = self.image_srcsets.get(srcset_name)
sourceset = srcset_config.get('sourceset')
if not srcset_config:
logger.warn(
"Could not find the given srcset_name {0}, leave tag untouched!".format(
srcset_name
)
)
return elem
sourceset = srcset_config.get("sourceset")
if not sourceset:
return elem
src = elem.attrs.get("src")
picture_tag = soup.new_tag("picture")
for i, source in enumerate(sourceset):
scale = source['scale']
media = source.get('media')
title = elem.attrs.get('title')
alt = elem.attrs.get('alt')
klass = elem.attrs.get('class')
scale = source["scale"]
media = source.get("media")
title = elem.attrs.get("title")
alt = elem.attrs.get("alt")
klass = elem.attrs.get("class")
if i == len(sourceset) - 1:
source_tag = soup.new_tag("img", src=self.update_src_scale(src=src, scale=scale))
source_tag = soup.new_tag(
"img", src=self.update_src_scale(src=src, scale=scale)
)
else:
# TODO guess type:
source_tag = soup.new_tag("source", srcset=self.update_src_scale(src=src, scale=scale))
source_tag = soup.new_tag(
"source", srcset=self.update_src_scale(src=src, scale=scale)
)
source_tag["loading"] = "lazy"
if media:
source_tag["media"] = media
@@ -98,4 +111,4 @@ def convert_to_srcset(self, srcset_name, elem, soup):

def update_src_scale(self, src, scale):
parts = src.split("/")
return "/".join(parts[:-1]) + "/{}".format(scale)
return "/".join(parts[:-1]) + "/{}".format(scale)
198 changes: 198 additions & 0 deletions plone/outputfilters/tests/test_image_srcset.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
# -*- coding: utf-8 -*-
from doctest import _ellipsis_match
from doctest import OutputChecker
from doctest import REPORT_NDIFF
from os.path import abspath
from os.path import dirname
from os.path import join
from plone.app.testing import setRoles
from plone.app.testing import TEST_USER_ID
from plone.app.testing.bbb import PloneTestCase
from plone.namedfile.file import NamedBlobImage
from plone.namedfile.file import NamedImage
from plone.namedfile.tests.test_scaling import DummyContent as NFDummyContent
from plone.outputfilters.filters.image_srcset import ImageSrcsetFilter
from plone.outputfilters.testing import PLONE_OUTPUTFILTERS_FUNCTIONAL_TESTING
from Products.PortalTransforms.tests.utils import normalize_html


PREFIX = abspath(dirname(__file__))


def dummy_image():
filename = join(PREFIX, u'image.jpg')
data = None
with open(filename, 'rb') as fd:
data = fd.read()
fd.close()
return NamedBlobImage(data=data, filename=filename)


class ImageSrcsetFilterIntegrationTestCase(PloneTestCase):

layer = PLONE_OUTPUTFILTERS_FUNCTIONAL_TESTING

image_id = 'image.jpg'

def _makeParser(self, **kw):
parser = ImageSrcsetFilter(context=self.portal)
for k, v in kw.items():
setattr(parser, k, v)
return parser

def _makeDummyContent(self):
from OFS.SimpleItem import SimpleItem

class DummyContent(SimpleItem):

def __init__(self, id):
self.id = id

def UID(self):
return 'foo'

allowedRolesAndUsers = ('Anonymous',)

class DummyContent2(NFDummyContent):
id = __name__ = 'foo2'
title = u'Schönes Bild'

def UID(self):
return 'foo2'

dummy = DummyContent('foo')
self.portal._setObject('foo', dummy)
self.portal.portal_catalog.catalog_object(self.portal.foo)

dummy2 = DummyContent2('foo2')
with open(join(PREFIX, self.image_id), 'rb') as fd:
data = fd.read()
fd.close()
dummy2.image = NamedImage(data, 'image/jpeg', u'image.jpeg')
self.portal._setObject('foo2', dummy2)
self.portal.portal_catalog.catalog_object(self.portal.foo2)

def _assertTransformsTo(self, input, expected):
# compare two chunks of HTML ignoring whitespace differences,
# and with a useful diff on failure
out = self.parser(input)
normalized_out = normalize_html(out)
normalized_expected = normalize_html(expected)
# print("e: {}".format(normalized_expected))
# print("o: {}".format(normalized_out))
try:
self.assertTrue(_ellipsis_match(normalized_expected,
normalized_out))
except AssertionError:
class wrapper(object):
want = expected
raise AssertionError(self.outputchecker.output_difference(
wrapper, out, REPORT_NDIFF))

def afterSetUp(self):
# create an image and record its UID
setRoles(self.portal, TEST_USER_ID, ['Manager'])

if self.image_id not in self.portal:
self.portal.invokeFactory(
'Image', id=self.image_id, title='Image')
image = self.portal[self.image_id]
image.setDescription('My caption')
image.image = dummy_image()
image.reindexObject()
self.UID = image.UID()
self.parser = self._makeParser(captioned_images=True,
resolve_uids=True)
assert self.parser.is_enabled()

self.outputchecker = OutputChecker()

def beforeTearDown(self):
self.login()
setRoles(self.portal, TEST_USER_ID, ['Manager'])
del self.portal[self.image_id]

def test_parsing_minimal(self):
text = '<div>Some simple text.</div>'
res = self.parser(text)
self.assertEqual(text, str(res))

def test_parsing_long_doc(self):
text = """<h1>Welcome!</h1>
<p class="discreet">If you're seeing this instead of the web site you were expecting, the owner of this web site has just installed Plone. Do not contact the Plone Team or the Plone support channels about this.</p>
<p class="discreet"><img class="image-richtext image-inline image-size-small" src="resolveuid/{uid}/@@images/image/preview" alt="" data-linktype="image" data-srcset="small" data-scale="preview" data-val="{uid}" /></p>
<h2>Get started</h2>
<p>Before you start exploring your newly created Plone site, please do the following:</p>
<ol>
<li>Make sure you are logged in as an admin/manager user. <span class="discreet">(You should have a Site Setup entry in the user menu)</span></li>
</ol>
<h2>Get comfortable</h2>
<p>After that, we suggest you do one or more of the following:</p>
<p><img class="image-richtext image-left image-size-medium captioned zoomable" src="resolveuid/{uid}/@@images/image/larger" alt="" data-linktype="image" data-srcset="medium" data-scale="larger" data-val="{uid}" /></p>
<h2>Make it your own</h2>
<p>Plone has a lot of different settings that can be used to make it do what you want it to. Some examples:</p>
<h2>Tell us how you use it</h2>
<p>Are you doing something interesting with Plone? Big site deployments, interesting use cases? Do you have a company that delivers Plone-based solutions?</p>
<h2>Find out more about Plone</h2>
<p class="discreet"><img class="image-richtext image-right image-size-large" src="resolveuid/{uid}/@@images/image/huge" alt="" data-linktype="image" data-srcset="large" data-scale="huge" data-val="{uid}" /></p>
<p>Plone is a powerful content management system built on a rock-solid application stack written using the Python programming language. More about these technologies:</p>
<h2><img class="image-richtext image-inline image-size-large" src="resolveuid/{uid}/@@images/image/huge" alt="" data-linktype="image" data-srcset="large" data-scale="huge" data-val="{uid}" /></h2>
<h2>Support the Plone Foundation</h2>
<p>Plone is made possible only through the efforts of thousands of dedicated individuals and hundreds of companies. The Plone Foundation:</p>
<ul>
<li>…protects and promotes Plone.</li>
<li>…is a registered 501(c)(3) charitable organization.</li>
<li>…donations are tax-deductible.</li>
</ul>
<p>Thanks for using our product; we hope you like it!</p>
<p>—The Plone Team</p>
""".format(uid=self.UID)
import time
startTime = time.time()
res = self.parser(text)
executionTime = (time.time() - startTime)
print("\n\nimage srcset parsing time: {}\n".format(executionTime))
self.assertTrue(res)

text_out = """<h1>Welcome!</h1>
<p class="discreet">If you're seeing this instead of the web site you were expecting, the owner of this web site has just installed Plone. Do not contact the Plone Team or the Plone support channels about this.</p>
<p class="discreet"><picture><img class="image-richtext image-inline image-size-small" loading="lazy" src="resolveuid/{uid}/@@images/image/preview"/></picture></p>
<h2>Get started</h2>
<p>Before you start exploring your newly created Plone site, please do the following:</p>
<ol>
<li>Make sure you are logged in as an admin/manager user. <span class="discreet">(You should have a Site Setup entry in the user menu)</span></li>
</ol>
<h2>Get comfortable</h2>
<p>After that, we suggest you do one or more of the following:</p>
<p><picture><source class="image-richtext image-left image-size-medium captioned zoomable" loading="lazy" media="(max-width:768px)" srcset="resolveuid/{uid}/@@images/image/large"/><img class="image-richtext image-left image-size-medium captioned zoomable" loading="lazy" src="resolveuid/{uid}/@@images/image/larger"/></picture></p>
<h2>Make it your own</h2>
<p>Plone has a lot of different settings that can be used to make it do what you want it to. Some examples:</p>
<h2>Tell us how you use it</h2>
<p>Are you doing something interesting with Plone? Big site deployments, interesting use cases? Do you have a company that delivers Plone-based solutions?</p>
<h2>Find out more about Plone</h2>
<p class="discreet"><picture><source class="image-richtext image-right image-size-large" loading="lazy" media="(max-width:768px) and (orientation:portrait)" srcset="resolveuid/{uid}/@@images/image/teaser"/><source class="image-richtext image-right image-size-large" loading="lazy" media="(max-width:768px)" srcset="resolveuid/{uid}/@@images/image/large"/><source class="image-richtext image-right image-size-large" loading="lazy" media="(min-width:992px)" srcset="resolveuid/{uid}/@@images/image/larger"/><source class="image-richtext image-right image-size-large" loading="lazy" media="(min-width:1200px)" srcset="resolveuid/{uid}/@@images/image/great"/><source class="image-richtext image-right image-size-large" loading="lazy" media="(min-width:1400px)" srcset="resolveuid/{uid}/@@images/image/huge"/><img class="image-richtext image-right image-size-large" loading="lazy" src="resolveuid/{uid}/@@images/image/huge"/></picture></p>
<p>Plone is a powerful content management system built on a rock-solid application stack written using the Python programming language. More about these technologies:</p>
<h2><picture><source class="image-richtext image-inline image-size-large" loading="lazy" media="(max-width:768px) and (orientation:portrait)" srcset="resolveuid/{uid}/@@images/image/teaser"/><source class="image-richtext image-inline image-size-large" loading="lazy" media="(max-width:768px)" srcset="resolveuid/{uid}/@@images/image/large"/><source class="image-richtext image-inline image-size-large" loading="lazy" media="(min-width:992px)" srcset="resolveuid/{uid}/@@images/image/larger"/><source class="image-richtext image-inline image-size-large" loading="lazy" media="(min-width:1200px)" srcset="resolveuid/{uid}/@@images/image/great"/><source class="image-richtext image-inline image-size-large" loading="lazy" media="(min-width:1400px)" srcset="resolveuid/{uid}/@@images/image/huge"/><img class="image-richtext image-inline image-size-large" loading="lazy" src="resolveuid/{uid}/@@images/image/huge"/></picture></h2>
<h2>Support the Plone Foundation</h2>
<p>Plone is made possible only through the efforts of thousands of dedicated individuals and hundreds of companies. The Plone Foundation:</p>
<ul>
<li>…protects and promotes Plone.</li>
<li>…is a registered 501(c)(3) charitable organization.</li>
<li>…donations are tax-deductible.</li>
</ul>
<p>Thanks for using our product; we hope you like it!</p>
<p>—The Plone Team</p>
""".format(uid=self.UID)
self._assertTransformsTo(text, text_out)

def test_parsing_with_nonexisting_srcset(self):
text = """
<p><img class="image-richtext image-inline image-size-thumb" src="resolveuid/{uid}/@@images/image/thumb" alt="" data-linktype="image" data-srcset="thumb" data-scale="thumb" data-val="{uid}" /></p>
""".format(uid=self.UID)
res = self.parser(text)
self.assertTrue(res)
text_out = """
<p><img class="image-richtext image-inline image-size-thumb" src="resolveuid/{uid}/@@images/image/thumb" alt="" data-linktype="image" data-srcset="thumb" data-scale="thumb" data-val="{uid}" /></p>
""".format(uid=self.UID)
# verify that tag was not converted:
self.assertTrue("data-srcset" in res)

0 comments on commit e700516

Please sign in to comment.