Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for allowed content types. #158

Merged
merged 2 commits into from Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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>