From eaa9f74c370adf233f230a9da75c77cb8af86b29 Mon Sep 17 00:00:00 2001 From: davisagli Date: Sat, 2 Sep 2023 22:12:12 -0700 Subject: [PATCH] [fc] Repository: plone.restapi Branch: refs/heads/main Date: 2023-09-02T22:12:12-07:00 Author: Andrea Cecchi (cekk) Commit: https://github.com/plone/plone.restapi/commit/36e67bd6c50053c71afdc34ff21fe23fdcd848a3 Improve RESOLVEUID_RE regexp to catch also paths generated by Link content-types (#1700) * Improve regexp to catch also paths generated by Link content-types * add changelog Files changed: A news/1699.bugfix M src/plone/restapi/serializer/utils.py M src/plone/restapi/tests/test_resolveuid.py --- last_commit.txt | 100 +++++++----------------------------------------- 1 file changed, 13 insertions(+), 87 deletions(-) diff --git a/last_commit.txt b/last_commit.txt index 1410751580..255376271e 100644 --- a/last_commit.txt +++ b/last_commit.txt @@ -1,95 +1,21 @@ -Repository: plone.namedfile +Repository: plone.restapi -Branch: refs/heads/master -Date: 2023-08-21T19:07:13+02:00 -Author: Marcel Liebischer (mliebischer) -Commit: https://github.com/plone/plone.namedfile/commit/18f39bb3e0b9eb284d855b576150e6bbf68fde85 +Branch: refs/heads/main +Date: 2023-09-02T22:12:12-07:00 +Author: Andrea Cecchi (cekk) +Commit: https://github.com/plone/plone.restapi/commit/36e67bd6c50053c71afdc34ff21fe23fdcd848a3 -Fix extracting dimensions of SVG files with lots of metadata (e.g. <svg> tag bigger than `MAX_INFO_BYTES`) +Improve RESOLVEUID_RE regexp to catch also paths generated by Link content-types (#1700) -Files changed: -A plone/namedfile/tests/image_large_header.svg -M plone/namedfile/file.py -M plone/namedfile/tests/__init__.py -M plone/namedfile/tests/test_blobfile.py -M plone/namedfile/tests/test_svg.py -M plone/namedfile/utils/svg_utils.py - -b'diff --git a/plone/namedfile/file.py b/plone/namedfile/file.py\nindex 7c2d1ef..f527aa1 100644\n--- a/plone/namedfile/file.py\n+++ b/plone/namedfile/file.py\n@@ -405,7 +405,8 @@ def _setData(self, data):\n super()._setData(data)\n firstbytes = self.getFirstBytes()\n res = getImageInfo(firstbytes)\n- if res == ("image/jpeg", -1, -1) or res == ("image/tiff", -1, -1):\n+ if res == ("image/jpeg", -1, -1) or res == ("image/tiff", -1, -1) or \\\n+ res == ("image/svg+xml", -1, -1):\n # header was longer than firstbytes\n start = len(firstbytes)\n length = max(0, MAX_INFO_BYTES - start)\ndiff --git a/plone/namedfile/tests/__init__.py b/plone/namedfile/tests/__init__.py\nindex 07f36b8..16d30e5 100644\n--- a/plone/namedfile/tests/__init__.py\n+++ b/plone/namedfile/tests/__init__.py\n@@ -1,8 +1,8 @@\n import os\n \n \n-def getFile(filename):\n+def getFile(filename, length=None):\n """return contents of the file with the given name"""\n filename = os.path.join(os.path.dirname(__file__), filename)\n with open(filename, "rb") as data_file:\n- return data_file.read()\n+ return data_file.read(length)\ndiff --git a/plone/namedfile/tests/image_large_header.svg b/plone/namedfile/tests/image_large_header.svg\nnew file mode 100644\nindex 0000000..ce53a0a\n--- /dev/null\n+++ b/plone/namedfile/tests/image_large_header.svg\n@@ -0,0 +1,661 @@\n+\n+\n+\n+\n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+
\n+
\n+ \n+

\n+ Object:Type\n+

\n+
\n+

field1 = value1
field2 = value2
field3 = value3\n+

\n+

field4 = value4\n+
\n+

\n+

field5 = value5\n+
\n+

\n+

field6 = value6\n+
\n+

\n+

field7 = value7\n+
\n+

\n+

field8 = value8\n+
\n+

\n+

field9 = value9\n+
\n+

\n+

field10 = value10

\n+
\n+
\n+ \n+
\n+ Object:Type...\n+
\n+
\n+ \n+ \n+ \n+ \n+ \n+
\n+
\n+ \n+

\n+ Object:Type\n+

\n+
\n+

field1 = value1
field2 = value2
field3 = value3\n+

\n+

field4 = value4\n+
\n+

\n+

field5 = value5\n+
\n+

\n+

field6 = value6\n+
\n+

\n+

field7 = value7\n+
\n+

\n+

field8 = value8

\n+
\n+
\n+ \n+
\n+ Object:Type...\n+
\n+
\n+ \n+ \n+ \n+ \n+ \n+ \n+
\n+
\n+ \n+

\n+ Object:Type\n+

\n+
\n+

field1 = value1
field2 = value2
field3 = value3\n+

\n+

field4 = value4\n+
\n+

\n+

field5 = value5\n+
\n+

\n+

field6 = value6\n+
\n+

\n+

field7 = value7\n+
\n+

\n+

field8 = value8\n+
\n+

\n+

field9 = value9\n+
\n+

\n+

field10 = value10

\n+
\n+
\n+ \n+
\n+ Object:Type...\n+
\n+
\n+ \n+ \n+ \n+ \n+ \n+ \n+
\n+
\n+ \n+

\n+ Object:Type\n+

\n+
\n+

field1 = value1
field2 = value2
field3 = value3\n+

\n+

field4 = value4\n+
\n+

\n+

field5 = value5\n+
\n+

\n+

field6 = value6\n+
\n+

\n+

field7 = value7\n+
\n+

\n+

field8 = value8\n+
\n+

\n+

field9 = value9\n+
\n+

\n+

field10 = value10\n+
\n+

\n+

field11 = value11\n+
\n+

\n+

field12 = value12

\n+
\n+
\n+ \n+
\n+ Object:Type...\n+
\n+
\n+ \n+ \n+ \n+ \n+ \n+
\n+
\n+ \n+

\n+ Object:Type\n+

\n+
\n+

field1 = value1
field2 = value2
field3 = value3\n+

\n+

field4 = value4\n+
\n+

\n+

field5 = value5\n+
\n+

\n+

field6 = value6\n+
\n+

\n+

field7 = value7\n+
\n+

\n+

field8 = value8

\n+
\n+
\n+ \n+
\n+ Object:Type...\n+
\n+
\n+ \n+ \n+ \n+ \n+ \n+
\n+
\n+ \n+

\n+ Object:Type\n+

\n+
\n+

field1 = value1
field2 = value2
field3 = value3\n+

\n+

field4 = value4

\n+
\n+
\n+ \n+
\n+ Object:Type...\n+
\n+
\n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+
\n+
\n+ \n+

\n+ Object:Type\n+

\n+
\n+

field1 = value1
field2 = value2
field3 = value3\n+

\n+

field4 = value4\n+
\n+

\n+

field5 = value5\n+
\n+

\n+

field6 = value6\n+
\n+

\n+

field7 = value7\n+
\n+

\n+

field8 = value8\n+
\n+

\n+

field9 = value9\n+
\n+

\n+

field10 = value10

\n+
\n+
\n+ \n+
\n+ Object:Type...\n+
\n+
\n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+
\n+
\n+ \n+

\n+ Object:Type\n+

\n+
\n+

field1 = value1
field2 = value2
field3 = value3\n+

\n+

field4 = value4\n+
\n+

\n+

field5 = value5\n+
\n+

\n+

field6 = value6\n+
\n+

\n+

field7 = value7\n+
\n+

\n+

field8 = value8

\n+
\n+
\n+ \n+
\n+ Object:Type...\n+
\n+
\n+ \n+ \n+ \n+ \n+ \n+
\n+
\n+ \n+

\n+ Object:Type\n+

\n+
\n+

field1 = value1
field2 = value2
field3 = value3\n+

\n+

field4 = value4\n+
\n+

\n+

field5 = value5\n+
\n+

\n+

field6 = value6\n+
\n+

\n+

field7 = value7\n+
\n+

\n+

field8 = value8\n+
\n+

\n+

field9 = value9\n+
\n+

\n+

field10 = value10\n+
\n+

\n+

field11 = value11\n+
\n+

\n+

field12 = value12

\n+
\n+
\n+ \n+
\n+ Object:Type...\n+
\n+
\n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+
\n+
\n+ \n+

\n+ Object:Type\n+

\n+
\n+

field1 = value1
field2 = value2
field3 = value3\n+

\n+

field4 = value4\n+
\n+

\n+

field5 = value5\n+
\n+

\n+

field6 = value6\n+
\n+

\n+

field7 = value7\n+
\n+

\n+

field8 = value8\n+
\n+

\n+

field9 = value9\n+
\n+

\n+

field10 = value10

\n+
\n+
\n+ \n+
\n+ Object:Type...\n+
\n+
\n+ \n+ \n+ \n+ \n+ \n+
\n+
\n+ \n+

\n+ Object:Type\n+

\n+
\n+

field1 = value1
field2 = value2
field3 = value3\n+

\n+

field4 = value4\n+
\n+

\n+

field5 = value5\n+
\n+

\n+

field6 = value6\n+
\n+

\n+

field7 = value7\n+
\n+

\n+

field8 = value8

\n+
\n+
\n+ \n+
\n+ Object:Type...\n+
\n+
\n+ \n+ \n+ \n+ \n+ \n+
\n+
\n+ \n+

\n+ Object:Type\n+

\n+
\n+

field1 = value1
field2 = value2
field3 = value3\n+

\n+

field4 = value4\n+
\n+

\n+

field5 = value5

\n+
\n+
\n+ \n+
\n+ Object:Type...\n+
\n+
\n+ \n+ \n+ \n+ \n+
\n+
\n+ \n+

\n+ Object:Type\n+

\n+
\n+

field1 = value1
field2 = value2
field3 = value3\n+

\n+

field4 = value4

\n+
\n+
\n+ \n+
\n+ Object:Type...\n+
\n+
\n+ \n+ \n+ \n+ \n+
\n+
\n+ \n+

\n+ Object:Type\n+

\n+
\n+

field1 = value1
field2 = value2
field3 = value3\n+

\n+

field4 = value4\n+
\n+

\n+

field5 = value5

\n+
\n+
\n+ \n+
\n+ Object:Type...\n+
\n+
\n+ \n+ \n+ \n+ \n+
\n+
\n+ \n+

\n+ Object:Type\n+

\n+
\n+

field1 = value1
field2 = value2
field3 = value3\n+

\n+

field4 = value4\n+
\n+

\n+

field5 = value5\n+
\n+

\n+

field6 = value6\n+
\n+

\n+

field7 = value7\n+
\n+

\n+

field8 = value8\n+
\n+

\n+

field9 = value9\n+
\n+

\n+

field10 = value10\n+
\n+

\n+

field11 = value11\n+
\n+

\n+

field12 = value12

\n+
\n+
\n+ \n+
\n+ Object:Type...\n+
\n+
\n+ \n+
\n+ \n+ \n+ \n+ Text is not SVG - cannot display\n+ \n+ \n+\n\\ No newline at end of file\ndiff --git a/plone/namedfile/tests/test_blobfile.py b/plone/namedfile/tests/test_blobfile.py\nindex d7df2fc..d748246 100644\n--- a/plone/namedfile/tests/test_blobfile.py\n+++ b/plone/namedfile/tests/test_blobfile.py\n@@ -27,6 +27,7 @@\n from zope.component import provideUtility\n from zope.interface.verify import verifyClass\n \n+import os\n import struct\n import transaction\n import unittest\n@@ -79,7 +80,7 @@ def testInterface(self):\n self.assertTrue(INamedBlobImage.implementedBy(NamedBlobImage))\n self.assertTrue(verifyClass(INamedBlobFile, NamedBlobImage))\n \n- def testDataMutatorWithLargeHeader(self):\n+ def testDataMutatorWithLargeJPGHeader(self):\n from plone.namedfile.file import IMAGE_INFO_BYTES\n \n bogus_header_length = struct.pack(">H", IMAGE_INFO_BYTES * 2)\n@@ -93,6 +94,23 @@ def testDataMutatorWithLargeHeader(self):\n image._setData(data)\n self.assertEqual(image.getImageSize(), (1024, 680))\n \n+ def testDataMutatorWithLargeSVGHeader(self):\n+ from plone.namedfile.file import IMAGE_INFO_BYTES\n+\n+ to_big_header_data = b\'d\' * (IMAGE_INFO_BYTES * 2)\n+\n+ data = (\n+ b\'\'\n+ b\'"\'\n+ )\n+ image = self._makeImage()\n+ image._setData(data)\n+ self.assertEqual(image.getImageSize(), (1024, 680))\n+ self.assertGreater(len(to_big_header_data), IMAGE_INFO_BYTES)\n+\n \n class TestImageFunctional(unittest.TestCase):\n \ndiff --git a/plone/namedfile/tests/test_svg.py b/plone/namedfile/tests/test_svg.py\nindex 0619f6c..cc2a31c 100644\n--- a/plone/namedfile/tests/test_svg.py\n+++ b/plone/namedfile/tests/test_svg.py\n@@ -1,11 +1,11 @@\n+import unittest\n+\n from plone.namedfile.file import NamedImage\n from plone.namedfile.tests import getFile\n from plone.namedfile.utils import get_contenttype\n from plone.namedfile.utils.svg_utils import dimension_int\n from plone.namedfile.utils.svg_utils import process_svg\n \n-import unittest\n-\n \n class TestSvg(unittest.TestCase):\n def test_get_contenttype(self):\n@@ -23,6 +23,25 @@ def test_process_svg(self):\n self.assertEqual(width, 158)\n self.assertEqual(height, 40)\n \n+ def test_process_svg__indicate_header_truncation(self):\n+ """ Check that we can detect SVG files where the file header was\n+ larger than the requested first bytes. process_svg() should\n+ return -1 as dimensions to indicate the truncation."""\n+\n+ truncated_data = getFile("image_large_header.svg", length=1024)\n+ content_type, width, height = process_svg(truncated_data)\n+ self.assertEqual(content_type, "image/svg+xml")\n+ self.assertEqual(width, -1)\n+ self.assertEqual(height, -1)\n+\n+ def test_process_svg__can_handle_large_header(self):\n+\n+ data = getFile("image_large_header.svg")\n+ content_type, width, height = process_svg(data)\n+ self.assertEqual(content_type, "image/svg+xml")\n+ self.assertEqual(width, 1041)\n+ self.assertEqual(height, 751)\n+\n def test_dimension_int(self):\n \n self.assertEqual(dimension_int("auto"), 0)\ndiff --git a/plone/namedfile/utils/svg_utils.py b/plone/namedfile/utils/svg_utils.py\nindex 004dedc..59c8dd5 100644\n--- a/plone/namedfile/utils/svg_utils.py\n+++ b/plone/namedfile/utils/svg_utils.py\n@@ -21,15 +21,16 @@ def process_svg(data):\n w = dimension_int(el.attrib.get("width"))\n h = dimension_int(el.attrib.get("height"))\n break\n- except et.ParseError:\n+ w = w if w > 1 else 1\n+ h = h if h > 1 else 1\n+ except et.ParseError as e:\n+ log.debug(f"Failed to parse SVG dimensions: {e}")\n pass\n \n if tag == "{http://www.w3.org/2000/svg}svg" or (\n size == 1024 and b"http://www.w3.org/2000/svg" in data\n ):\n content_type = "image/svg+xml"\n- w = w if w > 1 else 1\n- h = h if h > 1 else 1\n \n return content_type, w, h\n \n' - -Repository: plone.namedfile - - -Branch: refs/heads/master -Date: 2023-08-22T15:12:07+02:00 -Author: Marcel Liebischer (mliebischer) -Commit: https://github.com/plone/plone.namedfile/commit/2035c6b8a0eca79bef3ba6e06bf29552497ee3e0 - -Merge branch 'master' into mliebischer-support-svg-with-large-header - -Files changed: -M CHANGES.rst -M plone/namedfile/picture.py -M plone/namedfile/scaling.py -M plone/namedfile/storages.py -M plone/namedfile/tests/test_scaling.py -M plone/namedfile/tests/test_scaling_functional.py -M plone/namedfile/z3c-blobfile.zcml -M setup.py - -b'diff --git a/CHANGES.rst b/CHANGES.rst\nindex 6e04f96..1125697 100644\n--- a/CHANGES.rst\n+++ b/CHANGES.rst\n@@ -8,6 +8,42 @@ Changelog\n \n .. towncrier release notes start\n \n+6.1.1 (2023-06-22)\n+------------------\n+\n+Bug fixes:\n+\n+\n+- Return a 400 Bad Request response if the `@@images` view is published without a subpath. @davisagli (#144)\n+\n+\n+Tests\n+\n+\n+- Fix tests to work with various ``beautifulsoup4`` versions.\n+ [maurits] (#867)\n+\n+\n+6.1.0 (2023-05-22)\n+------------------\n+\n+New features:\n+\n+\n+- Move ``Zope2FileUploadStorable`` code from plone.app.z3cform to here to break a cyclic dependency.\n+ [gforcada] (#3764)\n+\n+\n+6.0.2 (2023-05-08)\n+------------------\n+\n+Bug fixes:\n+\n+\n+- Fix picture tag when original image is used instead of a scale.\n+ [maurits] (#142)\n+\n+\n 6.0.1 (2023-03-14)\n ------------------\n \ndiff --git a/plone/namedfile/picture.py b/plone/namedfile/picture.py\nindex 6b38786..14ea65b 100644\n--- a/plone/namedfile/picture.py\n+++ b/plone/namedfile/picture.py\n@@ -145,5 +145,15 @@ def update_src_scale(self, src, scale):\n src_scale = "/".join(parts[:-1]) + f"/{field_name}/{scale}"\n src_scale\n else:\n- src_scale = "/".join(parts[:-1]) + f"/{scale}"\n+ # Usually the url has \'@@images/fieldname/other_scale\',\n+ # and then we replace the other scale.\n+ # But the url may use the original image, e.g. @@images/image.\n+ # Then we want to keep the fieldname and return \'.../image/scale\'.\n+ try:\n+ full = len(parts) - parts.index("@@images") == 2\n+ except ValueError:\n+ full = False\n+ if not full:\n+ parts = parts[:-1]\n+ src_scale = "/".join(parts) + f"/{scale}"\n return src_scale\ndiff --git a/plone/namedfile/scaling.py b/plone/namedfile/scaling.py\nindex c21e8c8..9ec3bd0 100644\n--- a/plone/namedfile/scaling.py\n+++ b/plone/namedfile/scaling.py\n@@ -21,6 +21,7 @@\n from Products.CMFPlone.utils import safe_encode\n from Products.Five import BrowserView\n from xml.sax.saxutils import quoteattr\n+from zExceptions import BadRequest\n from zExceptions import Unauthorized\n from ZODB.blob import BlobFile\n from ZODB.POSException import ConflictError\n@@ -30,7 +31,7 @@\n from zope.deprecation import deprecate\n from zope.interface import alsoProvides\n from zope.interface import implementer\n-from zope.publisher.interfaces import IPublishTraverse\n+from zope.publisher.interfaces.browser import IBrowserPublisher\n from zope.publisher.interfaces import NotFound\n from zope.traversing.interfaces import ITraversable\n from zope.traversing.interfaces import TraversalError\n@@ -390,7 +391,7 @@ def __call__(\n return value, format_, dimensions\n \n \n-@implementer(ITraversable, IPublishTraverse)\n+@implementer(ITraversable, IBrowserPublisher)\n class ImageScaling(BrowserView):\n """view used for generating (and storing) image scales"""\n \n@@ -434,6 +435,10 @@ def publishTraverse(self, request, name):\n return scale_view\n raise NotFound(self, name, self.request)\n \n+ def browserDefault(self, request):\n+ # There\'s nothing in the path after /@@images\n+ raise BadRequest("Missing image scale path")\n+\n def traverse(self, name, furtherPath):\n """used for path traversal, i.e. in zope page templates"""\n # validate access\ndiff --git a/plone/namedfile/storages.py b/plone/namedfile/storages.py\nindex 5626200..6bb4c32 100644\n--- a/plone/namedfile/storages.py\n+++ b/plone/namedfile/storages.py\n@@ -105,3 +105,14 @@ def store(self, pdata, blob):\n fp = blob.open("w")\n fp.write(bytes(pdata))\n fp.close()\n+\n+\n+@implementer(IStorage)\n+class Zope2FileUploadStorable:\n+ def store(self, data, blob):\n+ data.seek(0)\n+ with blob.open("w") as fp:\n+ block = data.read(MAXCHUNKSIZE)\n+ while block:\n+ fp.write(block)\n+ block = data.read(MAXCHUNKSIZE)\ndiff --git a/plone/namedfile/tests/test_scaling.py b/plone/namedfile/tests/test_scaling.py\nindex ed4382a..7934763 100644\n--- a/plone/namedfile/tests/test_scaling.py\n+++ b/plone/namedfile/tests/test_scaling.py\n@@ -547,9 +547,17 @@ def testGetPictureTagByName(self, mock_uuid_to_object):\n http://nohost/item/@@images/image-800-....png 800w,\n http://nohost/item/@@images/image-1000-....png 1000w,\n http://nohost/item/@@images/image-1200-....png 1200w"/>\n- \n+ \n """\n- self.assertTrue(_ellipsis_match(expected, tag))\n+ self.assertTrue(_ellipsis_match(expected, tag.strip()))\n+\n+ # The exact placement of the img tag attributes can differ, especially\n+ # with different beautifulsoup versions.\n+ # So check here that all attributes are present.\n+ self.assertIn(\'height="200"\', tag)\n+ self.assertIn(\'loading="lazy"\', tag)\n+ self.assertIn(\'title="foo"\', tag)\n+ self.assertIn(\'width="200"\', tag)\n \n @patch.object(\n plone.namedfile.scaling,\n@@ -580,9 +588,18 @@ def testGetPictureTagWithAltAndTitle(self, mock_uuid_to_object):\n {base}/@@images/image-800-....png 800w,\n {base}/@@images/image-1000-....png 1000w,\n {base}/@@images/image-1200-....png 1200w"/>\n- Alternative text\n+ \n """\n- self.assertTrue(_ellipsis_match(expected, tag))\n+ self.assertTrue(_ellipsis_match(expected, tag.strip()))\n+\n+ # The exact placement of the img tag attributes can differ, especially\n+ # with different beautifulsoup versions.\n+ # So check here that all attributes are present.\n+ self.assertIn(\'alt="Alternative text"\', tag)\n+ self.assertIn(\'height="200"\', tag)\n+ self.assertIn(\'loading="lazy"\', tag)\n+ self.assertIn(\'title="Custom title"\', tag)\n+ self.assertIn(\'width="200"\', tag)\n \n @patch.object(\n plone.namedfile.scaling,\n@@ -601,8 +618,15 @@ def testGetPictureTagWithoutAnyVariants(self, mock_uuid_to_object):\n ImageScaling._sizes = patch_Img2PictureTag_allowed_scales()\n mock_uuid_to_object.return_value = self.item\n tag = self.scaling.picture("image", picture_variant="medium")\n- expected = """"""\n- self.assertTrue(_ellipsis_match(expected, tag))\n+ expected = """"""\n+ self.assertTrue(_ellipsis_match(expected, tag.strip()))\n+\n+ # The exact placement of the img tag attributes can differ, especially\n+ # with different beautifulsoup versions.\n+ # So check here that all attributes are present.\n+ self.assertIn(\'height="200"\', tag)\n+ self.assertIn(\'title="foo"\', tag)\n+ self.assertIn(\'width="200"\', tag)\n \n def testGetUnknownScale(self):\n foo = self.scaling.scale("image", scale="foo?")\n@@ -980,6 +1004,34 @@ def test_title(self):\n )\n \n \n+class Img2PictureTagTests(unittest.TestCase):\n+ """Low level tests for Img2PictureTag."""\n+\n+ def _makeOne(self):\n+ return plone.namedfile.picture.Img2PictureTag()\n+\n+ def test_update_src_scale(self):\n+ update_src_scale = self._makeOne().update_src_scale\n+ self.assertEqual(\n+ update_src_scale("foo/fieldname/old", "new"),\n+ "foo/fieldname/new"\n+ )\n+ self.assertEqual(\n+ update_src_scale("@@images/fieldname/old", "mini"),\n+ "@@images/fieldname/mini"\n+ )\n+ self.assertEqual(\n+ update_src_scale("@@images/fieldname", "preview"),\n+ "@@images/fieldname/preview"\n+ )\n+ self.assertEqual(\n+ update_src_scale(\n+ "photo.jpg/@@images/image-1200-4a03b0a8227d28737f5d9e3e481bdbd6.jpeg",\n+ "teaser"),\n+ "photo.jpg/@@images/image/teaser",\n+ )\n+\n+\n def test_suite():\n from unittest import defaultTestLoader\n \ndiff --git a/plone/namedfile/tests/test_scaling_functional.py b/plone/namedfile/tests/test_scaling_functional.py\nindex b9610ad..bf8f1fd 100644\n--- a/plone/namedfile/tests/test_scaling_functional.py\n+++ b/plone/namedfile/tests/test_scaling_functional.py\n@@ -10,6 +10,7 @@\n from plone.namedfile.testing import PLONE_NAMEDFILE_FUNCTIONAL_TESTING\n from plone.namedfile.tests import getFile\n from plone.testing.zope import Browser\n+from zExceptions import BadRequest\n from zope.annotation import IAttributeAnnotatable\n from zope.interface import implementer\n \n@@ -215,6 +216,11 @@ def testSVGPublishThumbViaName(self):\n self.assertEqual("image/svg+xml", self.browser.headers["content-type"])\n self.assertEqual(self.browser.contents, data)\n \n+ def testImagesViewWithNoSubpath(self):\n+ transaction.commit()\n+ with self.assertRaises(BadRequest):\n+ self.browser.open(self.layer["app"].absolute_url() + "/item/@@images")\n+\n \n def test_suite():\n from unittest import defaultTestLoader\ndiff --git a/plone/namedfile/z3c-blobfile.zcml b/plone/namedfile/z3c-blobfile.zcml\nindex 3b2ceb3..69a5312 100644\n--- a/plone/namedfile/z3c-blobfile.zcml\n+++ b/plone/namedfile/z3c-blobfile.zcml\n@@ -56,6 +56,12 @@\n factory=".storages.PDataStorable"\n />\n \n+ \n+\n \n \n \ndiff --git a/setup.py b/setup.py\nindex 803bd58..024a4a3 100644\n--- a/setup.py\n+++ b/setup.py\n@@ -4,7 +4,7 @@\n import os\n \n \n-version = "6.0.1"\n+version = "6.1.2.dev0"\n \n description = "File types and fields for images, files and blob files with " "filenames"\n long_description = "\\n\\n".join(\n' - -Repository: plone.namedfile - - -Branch: refs/heads/master -Date: 2023-08-22T16:44:18+02:00 -Author: Marcel Liebischer (mliebischer) -Commit: https://github.com/plone/plone.namedfile/commit/edcf90ab69a19530719c7709143d3d6c7829586f - -Add changenote - -Files changed: -A news/147.bugfix - -b'diff --git a/news/147.bugfix b/news/147.bugfix\nnew file mode 100644\nindex 0000000..5310330\n--- /dev/null\n+++ b/news/147.bugfix\n@@ -0,0 +1,6 @@\n+Fixed the issue where SVG images containing extensive metadata were not being displayed\n+correctly (resulting in a width/height of 1px). This problem could occur when the\n+ tag exceeded the MAX_INFO_BYTES limit.\n+\n+Fixes `issue 147 `_.\n+[mliebischer]\n\\ No newline at end of file\n' - -Repository: plone.namedfile - - -Branch: refs/heads/master -Date: 2023-08-22T16:44:42+02:00 -Author: Marcel Liebischer (mliebischer) -Commit: https://github.com/plone/plone.namedfile/commit/99c67897b2948b221c0196f35caaf6caa3dda08c - -Merge branch 'mliebischer-601-support-svg-with-large-header' into mliebischer-support-svg-with-large-header - -Files changed: -A news/147.bugfix - -b'diff --git a/news/147.bugfix b/news/147.bugfix\nnew file mode 100644\nindex 0000000..5310330\n--- /dev/null\n+++ b/news/147.bugfix\n@@ -0,0 +1,6 @@\n+Fixed the issue where SVG images containing extensive metadata were not being displayed\n+correctly (resulting in a width/height of 1px). This problem could occur when the\n+ tag exceeded the MAX_INFO_BYTES limit.\n+\n+Fixes `issue 147 `_.\n+[mliebischer]\n\\ No newline at end of file\n' - -Repository: plone.namedfile - - -Branch: refs/heads/master -Date: 2023-08-28T11:45:42+02:00 -Author: Maurits van Rees (mauritsvanrees) -Commit: https://github.com/plone/plone.namedfile/commit/2fe758af36ea14feb4b79ed984e01af16a4241bb - -Merge pull request #148 from plone/mliebischer-support-svg-with-large-header - -Add support for SVG files with large header +* Improve regexp to catch also paths generated by Link content-types + +* add changelog Files changed: -A news/147.bugfix -A plone/namedfile/tests/image_large_header.svg -M plone/namedfile/file.py -M plone/namedfile/tests/__init__.py -M plone/namedfile/tests/test_blobfile.py -M plone/namedfile/tests/test_svg.py -M plone/namedfile/utils/svg_utils.py +A news/1699.bugfix +M src/plone/restapi/serializer/utils.py +M src/plone/restapi/tests/test_resolveuid.py -b'diff --git a/news/147.bugfix b/news/147.bugfix\nnew file mode 100644\nindex 0000000..5310330\n--- /dev/null\n+++ b/news/147.bugfix\n@@ -0,0 +1,6 @@\n+Fixed the issue where SVG images containing extensive metadata were not being displayed\n+correctly (resulting in a width/height of 1px). This problem could occur when the\n+ tag exceeded the MAX_INFO_BYTES limit.\n+\n+Fixes `issue 147 `_.\n+[mliebischer]\n\\ No newline at end of file\ndiff --git a/plone/namedfile/file.py b/plone/namedfile/file.py\nindex 7c2d1ef..f527aa1 100644\n--- a/plone/namedfile/file.py\n+++ b/plone/namedfile/file.py\n@@ -405,7 +405,8 @@ def _setData(self, data):\n super()._setData(data)\n firstbytes = self.getFirstBytes()\n res = getImageInfo(firstbytes)\n- if res == ("image/jpeg", -1, -1) or res == ("image/tiff", -1, -1):\n+ if res == ("image/jpeg", -1, -1) or res == ("image/tiff", -1, -1) or \\\n+ res == ("image/svg+xml", -1, -1):\n # header was longer than firstbytes\n start = len(firstbytes)\n length = max(0, MAX_INFO_BYTES - start)\ndiff --git a/plone/namedfile/tests/__init__.py b/plone/namedfile/tests/__init__.py\nindex 07f36b8..16d30e5 100644\n--- a/plone/namedfile/tests/__init__.py\n+++ b/plone/namedfile/tests/__init__.py\n@@ -1,8 +1,8 @@\n import os\n \n \n-def getFile(filename):\n+def getFile(filename, length=None):\n """return contents of the file with the given name"""\n filename = os.path.join(os.path.dirname(__file__), filename)\n with open(filename, "rb") as data_file:\n- return data_file.read()\n+ return data_file.read(length)\ndiff --git a/plone/namedfile/tests/image_large_header.svg b/plone/namedfile/tests/image_large_header.svg\nnew file mode 100644\nindex 0000000..ce53a0a\n--- /dev/null\n+++ b/plone/namedfile/tests/image_large_header.svg\n@@ -0,0 +1,661 @@\n+\n+\n+\n+\n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+
\n+
\n+ \n+

\n+ Object:Type\n+

\n+
\n+

field1 = value1
field2 = value2
field3 = value3\n+

\n+

field4 = value4\n+
\n+

\n+

field5 = value5\n+
\n+

\n+

field6 = value6\n+
\n+

\n+

field7 = value7\n+
\n+

\n+

field8 = value8\n+
\n+

\n+

field9 = value9\n+
\n+

\n+

field10 = value10

\n+
\n+
\n+ \n+
\n+ Object:Type...\n+
\n+
\n+ \n+ \n+ \n+ \n+ \n+
\n+
\n+ \n+

\n+ Object:Type\n+

\n+
\n+

field1 = value1
field2 = value2
field3 = value3\n+

\n+

field4 = value4\n+
\n+

\n+

field5 = value5\n+
\n+

\n+

field6 = value6\n+
\n+

\n+

field7 = value7\n+
\n+

\n+

field8 = value8

\n+
\n+
\n+ \n+
\n+ Object:Type...\n+
\n+
\n+ \n+ \n+ \n+ \n+ \n+ \n+
\n+
\n+ \n+

\n+ Object:Type\n+

\n+
\n+

field1 = value1
field2 = value2
field3 = value3\n+

\n+

field4 = value4\n+
\n+

\n+

field5 = value5\n+
\n+

\n+

field6 = value6\n+
\n+

\n+

field7 = value7\n+
\n+

\n+

field8 = value8\n+
\n+

\n+

field9 = value9\n+
\n+

\n+

field10 = value10

\n+
\n+
\n+ \n+
\n+ Object:Type...\n+
\n+
\n+ \n+ \n+ \n+ \n+ \n+ \n+
\n+
\n+ \n+

\n+ Object:Type\n+

\n+
\n+

field1 = value1
field2 = value2
field3 = value3\n+

\n+

field4 = value4\n+
\n+

\n+

field5 = value5\n+
\n+

\n+

field6 = value6\n+
\n+

\n+

field7 = value7\n+
\n+

\n+

field8 = value8\n+
\n+

\n+

field9 = value9\n+
\n+

\n+

field10 = value10\n+
\n+

\n+

field11 = value11\n+
\n+

\n+

field12 = value12

\n+
\n+
\n+ \n+
\n+ Object:Type...\n+
\n+
\n+ \n+ \n+ \n+ \n+ \n+
\n+
\n+ \n+

\n+ Object:Type\n+

\n+
\n+

field1 = value1
field2 = value2
field3 = value3\n+

\n+

field4 = value4\n+
\n+

\n+

field5 = value5\n+
\n+

\n+

field6 = value6\n+
\n+

\n+

field7 = value7\n+
\n+

\n+

field8 = value8

\n+
\n+
\n+ \n+
\n+ Object:Type...\n+
\n+
\n+ \n+ \n+ \n+ \n+ \n+
\n+
\n+ \n+

\n+ Object:Type\n+

\n+
\n+

field1 = value1
field2 = value2
field3 = value3\n+

\n+

field4 = value4

\n+
\n+
\n+ \n+
\n+ Object:Type...\n+
\n+
\n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+
\n+
\n+ \n+

\n+ Object:Type\n+

\n+
\n+

field1 = value1
field2 = value2
field3 = value3\n+

\n+

field4 = value4\n+
\n+

\n+

field5 = value5\n+
\n+

\n+

field6 = value6\n+
\n+

\n+

field7 = value7\n+
\n+

\n+

field8 = value8\n+
\n+

\n+

field9 = value9\n+
\n+

\n+

field10 = value10

\n+
\n+
\n+ \n+
\n+ Object:Type...\n+
\n+
\n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+
\n+
\n+ \n+

\n+ Object:Type\n+

\n+
\n+

field1 = value1
field2 = value2
field3 = value3\n+

\n+

field4 = value4\n+
\n+

\n+

field5 = value5\n+
\n+

\n+

field6 = value6\n+
\n+

\n+

field7 = value7\n+
\n+

\n+

field8 = value8

\n+
\n+
\n+ \n+
\n+ Object:Type...\n+
\n+
\n+ \n+ \n+ \n+ \n+ \n+
\n+
\n+ \n+

\n+ Object:Type\n+

\n+
\n+

field1 = value1
field2 = value2
field3 = value3\n+

\n+

field4 = value4\n+
\n+

\n+

field5 = value5\n+
\n+

\n+

field6 = value6\n+
\n+

\n+

field7 = value7\n+
\n+

\n+

field8 = value8\n+
\n+

\n+

field9 = value9\n+
\n+

\n+

field10 = value10\n+
\n+

\n+

field11 = value11\n+
\n+

\n+

field12 = value12

\n+
\n+
\n+ \n+
\n+ Object:Type...\n+
\n+
\n+ \n+ \n+ \n+ \n+ \n+ \n+ \n+
\n+
\n+ \n+

\n+ Object:Type\n+

\n+
\n+

field1 = value1
field2 = value2
field3 = value3\n+

\n+

field4 = value4\n+
\n+

\n+

field5 = value5\n+
\n+

\n+

field6 = value6\n+
\n+

\n+

field7 = value7\n+
\n+

\n+

field8 = value8\n+
\n+

\n+

field9 = value9\n+
\n+

\n+

field10 = value10

\n+
\n+
\n+ \n+
\n+ Object:Type...\n+
\n+
\n+ \n+ \n+ \n+ \n+ \n+
\n+
\n+ \n+

\n+ Object:Type\n+

\n+
\n+

field1 = value1
field2 = value2
field3 = value3\n+

\n+

field4 = value4\n+
\n+

\n+

field5 = value5\n+
\n+

\n+

field6 = value6\n+
\n+

\n+

field7 = value7\n+
\n+

\n+

field8 = value8

\n+
\n+
\n+ \n+
\n+ Object:Type...\n+
\n+
\n+ \n+ \n+ \n+ \n+ \n+
\n+
\n+ \n+

\n+ Object:Type\n+

\n+
\n+

field1 = value1
field2 = value2
field3 = value3\n+

\n+

field4 = value4\n+
\n+

\n+

field5 = value5

\n+
\n+
\n+ \n+
\n+ Object:Type...\n+
\n+
\n+ \n+ \n+ \n+ \n+
\n+
\n+ \n+

\n+ Object:Type\n+

\n+
\n+

field1 = value1
field2 = value2
field3 = value3\n+

\n+

field4 = value4

\n+
\n+
\n+ \n+
\n+ Object:Type...\n+
\n+
\n+ \n+ \n+ \n+ \n+
\n+
\n+ \n+

\n+ Object:Type\n+

\n+
\n+

field1 = value1
field2 = value2
field3 = value3\n+

\n+

field4 = value4\n+
\n+

\n+

field5 = value5

\n+
\n+
\n+ \n+
\n+ Object:Type...\n+
\n+
\n+ \n+ \n+ \n+ \n+
\n+
\n+ \n+

\n+ Object:Type\n+

\n+
\n+

field1 = value1
field2 = value2
field3 = value3\n+

\n+

field4 = value4\n+
\n+

\n+

field5 = value5\n+
\n+

\n+

field6 = value6\n+
\n+

\n+

field7 = value7\n+
\n+

\n+

field8 = value8\n+
\n+

\n+

field9 = value9\n+
\n+

\n+

field10 = value10\n+
\n+

\n+

field11 = value11\n+
\n+

\n+

field12 = value12

\n+
\n+
\n+ \n+
\n+ Object:Type...\n+
\n+
\n+ \n+
\n+ \n+ \n+ \n+ Text is not SVG - cannot display\n+ \n+ \n+
\n\\ No newline at end of file\ndiff --git a/plone/namedfile/tests/test_blobfile.py b/plone/namedfile/tests/test_blobfile.py\nindex d7df2fc..d748246 100644\n--- a/plone/namedfile/tests/test_blobfile.py\n+++ b/plone/namedfile/tests/test_blobfile.py\n@@ -27,6 +27,7 @@\n from zope.component import provideUtility\n from zope.interface.verify import verifyClass\n \n+import os\n import struct\n import transaction\n import unittest\n@@ -79,7 +80,7 @@ def testInterface(self):\n self.assertTrue(INamedBlobImage.implementedBy(NamedBlobImage))\n self.assertTrue(verifyClass(INamedBlobFile, NamedBlobImage))\n \n- def testDataMutatorWithLargeHeader(self):\n+ def testDataMutatorWithLargeJPGHeader(self):\n from plone.namedfile.file import IMAGE_INFO_BYTES\n \n bogus_header_length = struct.pack(">H", IMAGE_INFO_BYTES * 2)\n@@ -93,6 +94,23 @@ def testDataMutatorWithLargeHeader(self):\n image._setData(data)\n self.assertEqual(image.getImageSize(), (1024, 680))\n \n+ def testDataMutatorWithLargeSVGHeader(self):\n+ from plone.namedfile.file import IMAGE_INFO_BYTES\n+\n+ to_big_header_data = b\'d\' * (IMAGE_INFO_BYTES * 2)\n+\n+ data = (\n+ b\'\'\n+ b\'"\'\n+ )\n+ image = self._makeImage()\n+ image._setData(data)\n+ self.assertEqual(image.getImageSize(), (1024, 680))\n+ self.assertGreater(len(to_big_header_data), IMAGE_INFO_BYTES)\n+\n \n class TestImageFunctional(unittest.TestCase):\n \ndiff --git a/plone/namedfile/tests/test_svg.py b/plone/namedfile/tests/test_svg.py\nindex 0619f6c..cc2a31c 100644\n--- a/plone/namedfile/tests/test_svg.py\n+++ b/plone/namedfile/tests/test_svg.py\n@@ -1,11 +1,11 @@\n+import unittest\n+\n from plone.namedfile.file import NamedImage\n from plone.namedfile.tests import getFile\n from plone.namedfile.utils import get_contenttype\n from plone.namedfile.utils.svg_utils import dimension_int\n from plone.namedfile.utils.svg_utils import process_svg\n \n-import unittest\n-\n \n class TestSvg(unittest.TestCase):\n def test_get_contenttype(self):\n@@ -23,6 +23,25 @@ def test_process_svg(self):\n self.assertEqual(width, 158)\n self.assertEqual(height, 40)\n \n+ def test_process_svg__indicate_header_truncation(self):\n+ """ Check that we can detect SVG files where the file header was\n+ larger than the requested first bytes. process_svg() should\n+ return -1 as dimensions to indicate the truncation."""\n+\n+ truncated_data = getFile("image_large_header.svg", length=1024)\n+ content_type, width, height = process_svg(truncated_data)\n+ self.assertEqual(content_type, "image/svg+xml")\n+ self.assertEqual(width, -1)\n+ self.assertEqual(height, -1)\n+\n+ def test_process_svg__can_handle_large_header(self):\n+\n+ data = getFile("image_large_header.svg")\n+ content_type, width, height = process_svg(data)\n+ self.assertEqual(content_type, "image/svg+xml")\n+ self.assertEqual(width, 1041)\n+ self.assertEqual(height, 751)\n+\n def test_dimension_int(self):\n \n self.assertEqual(dimension_int("auto"), 0)\ndiff --git a/plone/namedfile/utils/svg_utils.py b/plone/namedfile/utils/svg_utils.py\nindex 004dedc..59c8dd5 100644\n--- a/plone/namedfile/utils/svg_utils.py\n+++ b/plone/namedfile/utils/svg_utils.py\n@@ -21,15 +21,16 @@ def process_svg(data):\n w = dimension_int(el.attrib.get("width"))\n h = dimension_int(el.attrib.get("height"))\n break\n- except et.ParseError:\n+ w = w if w > 1 else 1\n+ h = h if h > 1 else 1\n+ except et.ParseError as e:\n+ log.debug(f"Failed to parse SVG dimensions: {e}")\n pass\n \n if tag == "{http://www.w3.org/2000/svg}svg" or (\n size == 1024 and b"http://www.w3.org/2000/svg" in data\n ):\n content_type = "image/svg+xml"\n- w = w if w > 1 else 1\n- h = h if h > 1 else 1\n \n return content_type, w, h\n \n' +b'diff --git a/news/1699.bugfix b/news/1699.bugfix\nnew file mode 100644\nindex 000000000..a0bdf39a1\n--- /dev/null\n+++ b/news/1699.bugfix\n@@ -0,0 +1 @@\n+Improve RESOLVEUID_RE regexp to catch also paths generated by Link content-types. @cekk\ndiff --git a/src/plone/restapi/serializer/utils.py b/src/plone/restapi/serializer/utils.py\nindex c2f89e4e2..e0c3bbdef 100644\n--- a/src/plone/restapi/serializer/utils.py\n+++ b/src/plone/restapi/serializer/utils.py\n@@ -8,7 +8,7 @@\n import re\n \n \n-RESOLVEUID_RE = re.compile("^[./]*resolve[Uu]id/([^/]*)/?(.*)$")\n+RESOLVEUID_RE = re.compile("^(?:|.*/)resolve[Uu]id/([^/]*)/?(.*)$")\n \n \n def resolve_uid(path):\ndiff --git a/src/plone/restapi/tests/test_resolveuid.py b/src/plone/restapi/tests/test_resolveuid.py\nindex 404639be8..1e42aa9bf 100644\n--- a/src/plone/restapi/tests/test_resolveuid.py\n+++ b/src/plone/restapi/tests/test_resolveuid.py\n@@ -24,7 +24,6 @@\n \n \n class TestBlocksResolveUIDFunctional(TestCase):\n-\n layer = PLONE_RESTAPI_BLOCKS_FUNCTIONAL_TESTING\n \n def setUp(self):\n@@ -698,3 +697,9 @@ def test_resolveuid_with_primary_field_url_keeps_suffix(self):\n ]["url"],\n self.doc2.absolute_url() + "/view",\n )\n+\n+ def test_resolveuid_handle_link_style_formats(self):\n+ uid = IUUID(self.doc2)\n+ blocks = {"aaa": {"@type": "foo", "url": f"/plone/resolveuid/{uid}"}}\n+ value = self.serialize("blocks", blocks)\n+ self.assertEqual(value["aaa"]["url"], self.doc2.absolute_url())\n'