Skip to content

Commit

Permalink
Merge pull request #158 from plone/allowed_content_types
Browse files Browse the repository at this point in the history
Support for allowed content types.
  • Loading branch information
mauritsvanrees committed Mar 15, 2024
2 parents e8a44b8 + af1c292 commit c1a80f7
Show file tree
Hide file tree
Showing 11 changed files with 523 additions and 116 deletions.
8 changes: 8 additions & 0 deletions news/157-2.feature
@@ -0,0 +1,8 @@
Improve contenttype detection logic for unregistered but common types.

Change get_contenttype to support common types which are or were not registered
with IANA, like image/webp or audio/midi.

Note: image/webp is already a IANA registered type and also added by
Products.MimetypesRegistry.
[thet]
7 changes: 7 additions & 0 deletions news/157.feature
@@ -0,0 +1,7 @@
Support for allowed media types.

Support to constrain files to specific media types with a "accept" attribute on
file and image fields, just like the "accept" attribute of the HTML file input.

Fixes: #157
[thet]
128 changes: 77 additions & 51 deletions plone/namedfile/field.py
Expand Up @@ -21,107 +21,133 @@
from zope.schema import Object
from zope.schema import ValidationError

import mimetypes

_ = MessageFactory("plone")


@implementer(IPluggableImageFieldValidation)
@adapter(INamedImageField, Interface)
class ImageContenttypeValidator:
class InvalidFile(ValidationError):
"""Exception for a invalid file."""

__doc__ = _("Invalid file")


class InvalidImageFile(ValidationError):
"""Exception for a invalid image file."""

__doc__ = _("Invalid image file")


class BinaryContenttypeValidator:
def __init__(self, field, value):
self.field = field
self.value = value

def __call__(self):
if self.value is None:
return
mimetype = get_contenttype(self.value)
if mimetype.split("/")[0] != "image":
raise InvalidImageFile(mimetype, self.field.__name__)

if not self.field.accept:
# No restrictions.
return

class InvalidImageFile(ValidationError):
"""Exception for invalid image file"""
mimetype = get_contenttype(self.value)

__doc__ = _("Invalid image file")
for accept in self.field.accept:
if accept[0] == ".":
# This is a file extension. Get a media type from it.
accept = mimetypes.guess_type(f"dummy{accept}", strict=False)[0]
if accept is None:
# This extension is unknown. Skip it.
continue

try:
accept_type, accept_subtype = accept.split("/")
content_type, content_subtype = mimetype.split("/")
except ValueError:
# The accept type is invalid. Skip it.
continue

def validate_binary_field(interface, field, value):
for name, validator in getAdapters((field, value), interface):
validator()
if accept_type == content_type and (
accept_subtype == content_subtype or accept_subtype == "*"
):
# This file is allowed, just don't raise a ValidationError.
return

# The file's content type is not allowed. Raise a ValidationError.
raise self.exception(mimetype, self.field.__name__)

def validate_image_field(field, value):
validate_binary_field(IPluggableImageFieldValidation, field, value)

@implementer(IPluggableFileFieldValidation)
@adapter(INamedFileField, Interface)
class FileContenttypeValidator(BinaryContenttypeValidator):
exception = InvalidFile

def validate_file_field(field, value):
validate_binary_field(IPluggableFileFieldValidation, field, value)

@implementer(IPluggableImageFieldValidation)
@adapter(INamedImageField, Interface)
class ImageContenttypeValidator(BinaryContenttypeValidator):
exception = InvalidImageFile

@implementer(INamedFileField)
class NamedFile(Object):
"""A NamedFile field"""

_type = FileValueType
schema = INamedFile
class NamedField(Object):

def __init__(self, **kw):
if "accept" in kw:
self.accept = kw.pop("accept")
if "schema" in kw:
self.schema = kw.pop("schema")
super().__init__(schema=self.schema, **kw)

def _validate(self, value):
super()._validate(value)
validate_file_field(self, value)
def validate(self, value, interface):
super().validate(value)
for name, validator in getAdapters((self, value), interface):
validator()


@implementer(INamedFileField)
class NamedFile(NamedField):
"""A NamedFile field"""

_type = FileValueType
schema = INamedFile
accept = ()

def validate(self, value):
super().validate(value, IPluggableFileFieldValidation)


@implementer(INamedImageField)
class NamedImage(Object):
class NamedImage(NamedField):
"""A NamedImage field"""

_type = ImageValueType
schema = INamedImage
accept = ("image/*",)

def __init__(self, **kw):
if "schema" in kw:
self.schema = kw.pop("schema")
super().__init__(schema=self.schema, **kw)

def _validate(self, value):
super()._validate(value)
validate_image_field(self, value)
def validate(self, value):
super().validate(value, IPluggableImageFieldValidation)


@implementer(INamedBlobFileField)
class NamedBlobFile(Object):
class NamedBlobFile(NamedField):
"""A NamedBlobFile field"""

_type = BlobFileValueType
schema = INamedBlobFile
accept = ()

def __init__(self, **kw):
if "schema" in kw:
self.schema = kw.pop("schema")
super().__init__(schema=self.schema, **kw)

def _validate(self, value):
super()._validate(value)
validate_file_field(self, value)
def validate(self, value):
super().validate(value, IPluggableFileFieldValidation)


@implementer(INamedBlobImageField)
class NamedBlobImage(Object):
class NamedBlobImage(NamedField):
"""A NamedBlobImage field"""

_type = BlobImageValueType
schema = INamedBlobImage
accept = ("image/*",)

def __init__(self, **kw):
if "schema" in kw:
self.schema = kw.pop("schema")
super().__init__(schema=self.schema, **kw)

def _validate(self, value):
super()._validate(value)
validate_image_field(self, value)
def validate(self, value):
super().validate(value, IPluggableImageFieldValidation)
7 changes: 6 additions & 1 deletion plone/namedfile/field.zcml
Expand Up @@ -3,9 +3,14 @@
xmlns:zcml="http://namespaces.zope.org/zcml"
xmlns:browser="http://namespaces.zope.org/browser">

<adapter
factory=".field.FileContenttypeValidator"
name="file_contenttype"
/>

<adapter
factory=".field.ImageContenttypeValidator"
name="image_contenttype"
/>

</configure>
</configure>

0 comments on commit c1a80f7

Please sign in to comment.