From 6687472bac29807942219a5fde4721f75fdb1132 Mon Sep 17 00:00:00 2001 From: Maurits van Rees Date: Fri, 27 Oct 2023 02:20:31 +0200 Subject: [PATCH 1/3] Be more strict when checking if mimetype is allowed to be displayed inline. This takes over some code from https://github.com/zopefoundation/Zope/pull/1167 --- news/1167.bugfix | 2 ++ plone/namedfile/browser.py | 3 ++- plone/namedfile/scaling.py | 3 ++- plone/namedfile/utils/__init__.py | 22 +++++++++++++++++++++- 4 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 news/1167.bugfix diff --git a/news/1167.bugfix b/news/1167.bugfix new file mode 100644 index 00000000..3d8ef860 --- /dev/null +++ b/news/1167.bugfix @@ -0,0 +1,2 @@ +Be more strict when checking if mimetype is allowed to be displayed inline. +[maurits] diff --git a/plone/namedfile/browser.py b/plone/namedfile/browser.py index a764a0a2..0930b752 100644 --- a/plone/namedfile/browser.py +++ b/plone/namedfile/browser.py @@ -1,4 +1,5 @@ from AccessControl.ZopeGuards import guarded_getattr +from plone.namedfile.utils import extract_media_type from plone.namedfile.utils import set_headers from plone.namedfile.utils import stream_data from plone.rfc822.interfaces import IPrimaryFieldInfo @@ -174,7 +175,7 @@ class DisplayFile(Download): def set_headers(self, file): if hasattr(file, "contentType"): - mimetype = file.contentType + mimetype = extract_media_type(file.contentType) if self.use_denylist: if mimetype in self.disallowed_inline_mimetypes: # Let the Download view handle this. diff --git a/plone/namedfile/scaling.py b/plone/namedfile/scaling.py index 47503ddc..8f2c9c4b 100644 --- a/plone/namedfile/scaling.py +++ b/plone/namedfile/scaling.py @@ -11,6 +11,7 @@ from plone.namedfile.interfaces import IStableImageScale from plone.namedfile.picture import get_picture_variants from plone.namedfile.picture import Img2PictureTag +from plone.namedfile.utils import extract_media_type from plone.namedfile.utils import getHighPixelDensityScales from plone.namedfile.utils import set_headers from plone.namedfile.utils import stream_data @@ -182,7 +183,7 @@ def _should_force_download(self): # If this returns True, the caller should call set_headers with a filename. if not hasattr(self.data, "contentType"): return - mimetype = self.data.contentType + mimetype = extract_media_type(self.data.contentType) if self.use_denylist: # We explicitly deny a few mimetypes, and allow the rest. return mimetype in self.disallowed_inline_mimetypes diff --git a/plone/namedfile/utils/__init__.py b/plone/namedfile/utils/__init__.py index a747a6c5..75a15bbe 100644 --- a/plone/namedfile/utils/__init__.py +++ b/plone/namedfile/utils/__init__.py @@ -94,7 +94,11 @@ def safe_basename(filename): def get_contenttype(file=None, filename=None, default="application/octet-stream"): - """Get the MIME content type of the given file and/or filename.""" + """Get the MIME content type of the given file and/or filename. + + Note: depending on your use case, you may want to call 'extract_media_type' + on the result. + """ file_type = getattr(file, "contentType", None) if file_type: @@ -108,6 +112,22 @@ def get_contenttype(file=None, filename=None, default="application/octet-stream" return default +def extract_media_type(content_type): + """extract the proper media type from *content_type*. + + Ignore parameters and whitespace and normalize to lower case. + See https://github.com/zopefoundation/Zope/pull/1167 + """ + if not content_type: + return content_type + # ignore parameters + content_type = content_type.split(";", 1)[0] + # ignore whitespace + content_type = "".join(content_type.split()) + # normalize to lowercase + return content_type.lower() + + def set_headers(file, response, filename=None): """Set response headers for the given file. If filename is given, set the Content-Disposition to attachment. From 17dbec4aa50d86370af8328977808141bd90c094 Mon Sep 17 00:00:00 2001 From: Maurits van Rees Date: Fri, 27 Oct 2023 09:32:21 +0200 Subject: [PATCH 2/3] Only define extract_media_type when we cannot import it from Zope. --- plone/namedfile/utils/__init__.py | 36 +++++++++++++++++-------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/plone/namedfile/utils/__init__.py b/plone/namedfile/utils/__init__.py index 75a15bbe..b27c8820 100644 --- a/plone/namedfile/utils/__init__.py +++ b/plone/namedfile/utils/__init__.py @@ -31,6 +31,26 @@ except ImportError: from Products.CMFPlone.interfaces.controlpanel import IImagingSchema +try: + # Zope 5.8.6+ + from OFS.Image import extract_media_type +except ImportError: + + def extract_media_type(content_type): + """extract the proper media type from *content_type*. + + Ignore parameters and whitespace and normalize to lower case. + See https://github.com/zopefoundation/Zope/pull/1167 + """ + if not content_type: + return content_type + # ignore parameters + content_type = content_type.split(";", 1)[0] + # ignore whitespace + content_type = "".join(content_type.split()) + # normalize to lowercase + return content_type.lower() + @implementer(IStreamIterator) class filestream_range_iterator(Iterable): @@ -112,22 +132,6 @@ def get_contenttype(file=None, filename=None, default="application/octet-stream" return default -def extract_media_type(content_type): - """extract the proper media type from *content_type*. - - Ignore parameters and whitespace and normalize to lower case. - See https://github.com/zopefoundation/Zope/pull/1167 - """ - if not content_type: - return content_type - # ignore parameters - content_type = content_type.split(";", 1)[0] - # ignore whitespace - content_type = "".join(content_type.split()) - # normalize to lowercase - return content_type.lower() - - def set_headers(file, response, filename=None): """Set response headers for the given file. If filename is given, set the Content-Disposition to attachment. From ff616cced3aca4651c0d4814e31b4d0001362420 Mon Sep 17 00:00:00 2001 From: Maurits van Rees Date: Fri, 27 Oct 2023 09:35:08 +0200 Subject: [PATCH 3/3] Copy unit test for extract_media_type from Zope. --- plone/namedfile/tests/test_image.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/plone/namedfile/tests/test_image.py b/plone/namedfile/tests/test_image.py index 8360a975..d05a42b5 100644 --- a/plone/namedfile/tests/test_image.py +++ b/plone/namedfile/tests/test_image.py @@ -1,4 +1,4 @@ -# This file is borrowed from zope.app.file and licensed ZPL. +# This file is partially borrowed from zope.app.file and licensed ZPL. from DateTime import DateTime from plone.namedfile.file import NamedImage @@ -82,6 +82,15 @@ def testInterface(self): self.assertTrue(INamedImage.implementedBy(NamedImage)) self.assertTrue(verifyClass(INamedImage, NamedImage)) + def test_extract_media_type(self): + from plone.namedfile.utils import extract_media_type as extract + + self.assertIsNone(extract(None)) + self.assertEqual(extract("text/plain"), "text/plain") + self.assertEqual(extract("TEXT/PLAIN"), "text/plain") + self.assertEqual(extract("text / plain"), "text/plain") + self.assertEqual(extract(" text/plain ; charset=utf-8"), "text/plain") + def test_get_contenttype(self): self.assertEqual( get_contenttype(NamedImage(getFile("image.gif"), contentType="image/gif")),