Skip to content

Commit

Permalink
Implement accepted media types.
Browse files Browse the repository at this point in the history
Support to constrain files to specific media types with a "accepted" attribute
on file and image fields.

Fixes: #157
  • Loading branch information
thet committed Mar 14, 2024
1 parent e8a44b8 commit 591d0ae
Show file tree
Hide file tree
Showing 8 changed files with 446 additions and 93 deletions.
3 changes: 3 additions & 0 deletions news/157.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Support to constrain files to specific media types with a "accepted" attribute on file and image fields.
Fixes: #157
[thet]
128 changes: 77 additions & 51 deletions plone/namedfile/field.py
Original file line number Diff line number Diff line change
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.accepted:
# No restrictions.
return

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

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

try:
accepted_type, accepted_subtype = accepted.split("/")
content_type, content_subtype = mimetype.split("/")
except ValueError:
# The accepted type is invalid. Skip it.
continue

def validate_binary_field(interface, field, value):
for name, validator in getAdapters((field, value), interface):
validator()
if accepted_type == content_type and (
accepted_subtype == content_subtype or accepted_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 "accepted" in kw:
self.accepted = kw.pop("accepted")
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
accepted = ()

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


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

_type = ImageValueType
schema = INamedImage
accepted = ("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
accepted = ()

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
accepted = ("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
Original file line number Diff line number Diff line change
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 591d0ae

Please sign in to comment.