Anna Sirota
ba1f69eced
From this change onwards only pre-generated avatar thumbnails are used anywhere in Blender ID. No implicit on-demand thumbnail generation or caching (or trashing of the cache table on each lookup of a thumbnail) should be going on.
100 lines
3.2 KiB
Python
100 lines
3.2 KiB
Python
"""Custom form and model fields."""
|
|
from io import BytesIO
|
|
import sys
|
|
import xml.etree.cElementTree as et
|
|
|
|
from django.core.exceptions import ValidationError
|
|
from django.core.validators import FileExtensionValidator, get_available_image_extensions
|
|
from django.db import models
|
|
from django.db.models.fields.files import ImageFieldFile
|
|
from django.forms import ImageField
|
|
|
|
import sorl.thumbnail
|
|
|
|
|
|
class AvatarFieldFile(ImageFieldFile): # keeping it here because it's used in migrations
|
|
pass
|
|
|
|
|
|
class AvatarField(sorl.thumbnail.ImageField):
|
|
"""Sorl Thumbnail for user avatars."""
|
|
|
|
attr_class = AvatarFieldFile
|
|
|
|
|
|
def validate_image_and_svg_file_extension(value):
|
|
"""Allow images and SVG."""
|
|
allowed_extensions = get_available_image_extensions() + ['svg']
|
|
return FileExtensionValidator(allowed_extensions=allowed_extensions)(value)
|
|
|
|
|
|
class SVGAndImageFormField(ImageField):
|
|
"""Validate uploaded file and allow images and SVG."""
|
|
|
|
default_validators = [validate_image_and_svg_file_extension]
|
|
|
|
def to_python(self, data):
|
|
"""Do what ImageField does, then check for SVG and allow it."""
|
|
f = super(ImageField, self).to_python(data)
|
|
if f is None:
|
|
return None
|
|
|
|
# Do whatever ImageField does first
|
|
from PIL import Image
|
|
|
|
# We need to get a file object for Pillow. We might have a path or we might
|
|
# have to read the data into memory.
|
|
if hasattr(data, 'temporary_file_path'):
|
|
file = data.temporary_file_path()
|
|
else:
|
|
if hasattr(data, 'read'):
|
|
file = BytesIO(data.read())
|
|
else:
|
|
file = BytesIO(data['content'])
|
|
|
|
try:
|
|
# load() could spot a truncated JPEG, but it loads the entire
|
|
# image in memory, which is a DoS vector. See #3848 and #18520.
|
|
image = Image.open(file)
|
|
# verify() must be called immediately after the constructor.
|
|
image.verify()
|
|
|
|
# Annotating so subclasses can reuse it for their own validation
|
|
f.image = image
|
|
# Pillow doesn't detect the MIME type of all formats. In those
|
|
# cases, content_type will be None.
|
|
f.content_type = Image.MIME.get(image.format)
|
|
except Exception:
|
|
# Add a workaround to handle SVG images
|
|
if not self.is_svg(file):
|
|
raise ValidationError(
|
|
self.error_messages["invalid_image"], code="invalid_image"
|
|
).with_traceback(sys.exc_info()[2])
|
|
if hasattr(f, "seek") and callable(f.seek):
|
|
f.seek(0)
|
|
return f
|
|
|
|
def is_svg(self, f):
|
|
"""Check if provided file is svg."""
|
|
f.seek(0)
|
|
tag = None
|
|
try:
|
|
for event, el in et.iterparse(f, ("start",)):
|
|
tag = el.tag
|
|
break
|
|
except et.ParseError:
|
|
pass
|
|
return tag == "{http://www.w3.org/2000/svg}svg"
|
|
|
|
|
|
class SVGAndImageField(models.ImageField):
|
|
"""Override validation with SVGAndImageFormField."""
|
|
|
|
def formfield(self, **kwargs):
|
|
return super().formfield(
|
|
**{
|
|
'form_class': SVGAndImageFormField,
|
|
**kwargs,
|
|
}
|
|
)
|