Fixed #21548 -- Added FileExtensionValidator and validate_image_file_extension.

This commit is contained in:
Berker Peksag 2016-03-26 22:09:08 +02:00 committed by Tim Graham
parent c9d0a0f7f4
commit 12b4280444
6 changed files with 141 additions and 6 deletions

View File

@ -1,5 +1,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import os
import re import re
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
@ -452,3 +453,53 @@ class DecimalValidator(object):
self.max_digits == other.max_digits and self.max_digits == other.max_digits and
self.decimal_places == other.decimal_places self.decimal_places == other.decimal_places
) )
@deconstructible
class FileExtensionValidator(object):
message = _(
"File extension '%(extension)s' is not allowed. "
"Allowed extensions are: '%(allowed_extensions)s'."
)
code = 'invalid_extension'
def __init__(self, allowed_extensions=None, message=None, code=None):
self.allowed_extensions = allowed_extensions
if message is not None:
self.message = message
if code is not None:
self.code = code
def __call__(self, value):
extension = os.path.splitext(value.name)[1][1:].lower()
if self.allowed_extensions is not None and extension not in self.allowed_extensions:
raise ValidationError(
self.message,
code=self.code,
params={
'extension': extension,
'allowed_extensions': ', '.join(self.allowed_extensions)
}
)
def __eq__(self, other):
return (
isinstance(other, self.__class__) and
self.allowed_extensions == other.allowed_extensions and
self.message == other.message and
self.code == other.code
)
def get_available_image_extensions():
try:
from PIL import Image
except ImportError:
return []
else:
Image.init()
return [ext.lower()[1:] for ext in Image.EXTENSION.keys()]
validate_image_file_extension = FileExtensionValidator(
allowed_extensions=get_available_image_extensions(),
)

View File

@ -8,6 +8,7 @@ from django.core import checks
from django.core.files.base import File from django.core.files.base import File
from django.core.files.images import ImageFile from django.core.files.images import ImageFile
from django.core.files.storage import default_storage from django.core.files.storage import default_storage
from django.core.validators import validate_image_file_extension
from django.db.models import signals from django.db.models import signals
from django.db.models.fields import Field from django.db.models.fields import Field
from django.utils import six from django.utils import six
@ -378,6 +379,7 @@ class ImageFieldFile(ImageFile, FieldFile):
class ImageField(FileField): class ImageField(FileField):
default_validators = [validate_image_file_extension]
attr_class = ImageFieldFile attr_class = ImageFieldFile
descriptor_class = ImageFileDescriptor descriptor_class = ImageFileDescriptor
description = _("Image") description = _("Image")

View File

@ -279,3 +279,30 @@ to, or in lieu of custom ``field.clean()`` methods.
``decimal_places``. ``decimal_places``.
- ``'max_whole_digits'`` if the number of whole digits is larger than - ``'max_whole_digits'`` if the number of whole digits is larger than
the difference between ``max_digits`` and ``decimal_places``. the difference between ``max_digits`` and ``decimal_places``.
``FileExtensionValidator``
--------------------------
.. class:: FileExtensionValidator(allowed_extensions, message, code)
.. versionadded:: 1.11
Raises a :exc:`~django.core.exceptions.ValidationError` with a code of
``'invalid_extension'`` if the ``value`` cannot be found in
``allowed_extensions``.
.. warning::
Don't rely on validation of the file extension to determine a file's
type. Files can be renamed to have any extension no matter what data
they contain.
``validate_image_file_extension``
---------------------------------
.. data:: validate_image_file_extension
.. versionadded:: 1.11
Uses Pillow to ensure that the ``value`` is `a valid image extension
<https://pillow.readthedocs.org/en/latest/handbook/image-file-formats.html>`_.

View File

@ -192,6 +192,9 @@ Models
<django.db.models.query.QuerySet.update_or_create>` and <django.db.models.query.QuerySet.update_or_create>` and
:meth:`~django.db.models.query.QuerySet.get_or_create`. :meth:`~django.db.models.query.QuerySet.get_or_create`.
* :class:`~django.db.models.ImageField` now has a default
:data:`~django.core.validators.validate_image_file_extension` validator.
Requests and Responses Requests and Responses
~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~
@ -237,7 +240,10 @@ URLs
Validators Validators
~~~~~~~~~~ ~~~~~~~~~~
* ... * Added :class:`~django.core.validators.FileExtensionValidator` to validate
file extensions and
:data:`~django.core.validators.validate_image_file_extension` to validate
image files.
.. _backwards-incompatible-1.11: .. _backwards-incompatible-1.11:

View File

@ -4,7 +4,7 @@ import os
import shutil import shutil
from unittest import skipIf from unittest import skipIf
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured, ValidationError
from django.core.files import File from django.core.files import File
from django.core.files.images import ImageFile from django.core.files.images import ImageFile
from django.test import TestCase from django.test import TestCase
@ -133,6 +133,12 @@ class ImageFieldTests(ImageFieldTestMixin, TestCase):
self.assertEqual(hash(p1_db.mugshot), hash(p1.mugshot)) self.assertEqual(hash(p1_db.mugshot), hash(p1.mugshot))
self.assertIs(p1_db.mugshot != p1.mugshot, False) self.assertIs(p1_db.mugshot != p1.mugshot, False)
def test_validation(self):
p = self.PersonModel(name="Joan")
p.mugshot.save("shot.txt", self.file1)
with self.assertRaisesMessage(ValidationError, "File extension 'txt' is not allowed."):
p.full_clean()
def test_instantiate_missing(self): def test_instantiate_missing(self):
""" """
If the underlying file is unavailable, still create instantiate the If the underlying file is unavailable, still create instantiate the

View File

@ -9,11 +9,13 @@ from datetime import datetime, timedelta
from unittest import TestCase from unittest import TestCase
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.files.base import ContentFile
from django.core.validators import ( from django.core.validators import (
BaseValidator, DecimalValidator, EmailValidator, MaxLengthValidator, BaseValidator, DecimalValidator, EmailValidator, FileExtensionValidator,
MaxValueValidator, MinLengthValidator, MinValueValidator, RegexValidator, MaxLengthValidator, MaxValueValidator, MinLengthValidator,
URLValidator, int_list_validator, validate_comma_separated_integer_list, MinValueValidator, RegexValidator, URLValidator, int_list_validator,
validate_email, validate_integer, validate_ipv4_address, validate_comma_separated_integer_list, validate_email,
validate_image_file_extension, validate_integer, validate_ipv4_address,
validate_ipv6_address, validate_ipv46_address, validate_slug, validate_ipv6_address, validate_ipv46_address, validate_slug,
validate_unicode_slug, validate_unicode_slug,
) )
@ -242,6 +244,17 @@ TEST_DATA = [
(RegexValidator('x', flags=re.IGNORECASE), 'y', ValidationError), (RegexValidator('x', flags=re.IGNORECASE), 'y', ValidationError),
(RegexValidator('a'), 'A', ValidationError), (RegexValidator('a'), 'A', ValidationError),
(RegexValidator('a', flags=re.IGNORECASE), 'A', None), (RegexValidator('a', flags=re.IGNORECASE), 'A', None),
(FileExtensionValidator(['txt']), ContentFile('contents', name='fileWithUnsupportedExt.jpg'), ValidationError),
(FileExtensionValidator(['txt']), ContentFile('contents', name='fileWithNoExtenstion'), ValidationError),
(FileExtensionValidator([]), ContentFile('contents', name='file.txt'), ValidationError),
(FileExtensionValidator(['txt']), ContentFile('contents', name='file.txt'), None),
(FileExtensionValidator(), ContentFile('contents', name='file.jpg'), None),
(validate_image_file_extension, ContentFile('contents', name='file.jpg'), None),
(validate_image_file_extension, ContentFile('contents', name='file.png'), None),
(validate_image_file_extension, ContentFile('contents', name='file.txt'), ValidationError),
(validate_image_file_extension, ContentFile('contents', name='file'), ValidationError),
] ]
@ -422,3 +435,33 @@ class TestValidatorEquality(TestCase):
DecimalValidator(1, 2), DecimalValidator(1, 2),
MinValueValidator(11), MinValueValidator(11),
) )
def test_file_extension_equality(self):
self.assertEqual(
FileExtensionValidator(),
FileExtensionValidator()
)
self.assertEqual(
FileExtensionValidator(['txt']),
FileExtensionValidator(['txt'])
)
self.assertEqual(
FileExtensionValidator(['txt']),
FileExtensionValidator(['txt'], code='invalid_extension')
)
self.assertNotEqual(
FileExtensionValidator(['txt']),
FileExtensionValidator(['png'])
)
self.assertNotEqual(
FileExtensionValidator(['txt']),
FileExtensionValidator(['png', 'jpg'])
)
self.assertNotEqual(
FileExtensionValidator(['txt']),
FileExtensionValidator(['txt'], code='custom_code')
)
self.assertNotEqual(
FileExtensionValidator(['txt']),
FileExtensionValidator(['txt'], message='custom error message')
)