mirror of
https://github.com/Hopiu/django-markdownx.git
synced 2026-03-16 21:40:24 +00:00
189 lines
6.7 KiB
Python
Executable file
189 lines
6.7 KiB
Python
Executable file
from os import path, SEEK_END, SEEK_SET
|
|
from io import BytesIO
|
|
from uuid import uuid4
|
|
from collections import namedtuple
|
|
|
|
from django import forms
|
|
from django.core.files.storage import default_storage
|
|
from django.core.files.uploadedfile import InMemoryUploadedFile
|
|
|
|
from .utils import scale_and_crop, xml_has_javascript
|
|
from .exceptions import MarkdownxImageUploadError
|
|
|
|
from .settings import (
|
|
MARKDOWNX_IMAGE_MAX_SIZE,
|
|
MARKDOWNX_MEDIA_PATH,
|
|
MARKDOWNX_UPLOAD_CONTENT_TYPES,
|
|
MARKDOWNX_UPLOAD_MAX_SIZE,
|
|
MARKDOWNX_SVG_JAVASCRIPT_PROTECTION
|
|
)
|
|
|
|
|
|
class ImageForm(forms.Form):
|
|
"""
|
|
Used for the handling of images uploaded using the editor through :guilabel:`AJAX`.
|
|
"""
|
|
|
|
image = forms.FileField()
|
|
|
|
# Separately defined as it needs to be
|
|
# processed a text file rather than image.
|
|
_SVG_TYPE = 'image/svg+xml'
|
|
|
|
def save(self, commit=True):
|
|
"""
|
|
Saves the uploaded image in the designated location.
|
|
|
|
If image type is not SVG, a byteIO of image content_type is created and
|
|
subsequently save; otherwise, the SVG is saved in its existing ``charset``
|
|
as an ``image/svg+xml``.
|
|
|
|
*Note*: The dimension of image files (excluding SVG) are set using ``PIL``.
|
|
|
|
:param commit: If ``True``, the file is saved to the disk;
|
|
otherwise, it is held in the memory.
|
|
:type commit: bool
|
|
:return: An instance of saved image if ``commit is True``,
|
|
else ``namedtuple(path, image)``.
|
|
:rtype: bool, namedtuple
|
|
"""
|
|
image = self.files.get('image')
|
|
content_type = image.content_type
|
|
file_name = image.name
|
|
image_extension = content_type.split('/')[-1].upper()
|
|
image_size = image.size
|
|
|
|
if content_type.lower() != self._SVG_TYPE:
|
|
# Processing the raster graphic image.
|
|
# Note that vector graphics in SVG format
|
|
# do not require additional processing and
|
|
# may be stored as uploaded.
|
|
image = self._process_raster(image, image_extension)
|
|
image_size = image.tell()
|
|
image.seek(0, SEEK_SET)
|
|
|
|
# Processed file (or the actual file in the case of SVG) is now
|
|
# saved in the memory as a Django object.
|
|
uploaded_image = InMemoryUploadedFile(
|
|
file=image,
|
|
field_name=None,
|
|
name=file_name,
|
|
content_type=content_type,
|
|
size=image_size,
|
|
charset=None
|
|
)
|
|
|
|
if (content_type.lower() == self._SVG_TYPE
|
|
and MARKDOWNX_SVG_JAVASCRIPT_PROTECTION
|
|
and xml_has_javascript(uploaded_image.read())):
|
|
|
|
raise MarkdownxImageUploadError(
|
|
'Failed security monitoring: SVG file contains JavaScript.'
|
|
)
|
|
|
|
return self._save(uploaded_image, file_name, commit)
|
|
|
|
def _save(self, image, file_name, commit):
|
|
"""
|
|
Final saving process, called internally after processing tasks are complete.
|
|
|
|
:param image: Prepared image
|
|
:type image: django.core.files.uploadedfile.InMemoryUploadedFile
|
|
:param file_name: Name of the file using which the image is to be saved.
|
|
:type file_name: str
|
|
:param commit: If ``True``, the image is saved onto the disk.
|
|
:type commit: bool
|
|
:return: URL of the uploaded image ``commit=True``, otherwise a namedtuple
|
|
of ``(path, image)`` where ``path`` is the absolute path generated
|
|
for saving the file, and ``image`` is the prepared image.
|
|
:rtype: str, namedtuple
|
|
"""
|
|
# Defining a universally unique name for the file
|
|
# to be saved on the disk.
|
|
unique_file_name = self.get_unique_file_name(file_name)
|
|
full_path = path.join(MARKDOWNX_MEDIA_PATH, unique_file_name)
|
|
|
|
if commit:
|
|
default_storage.save(full_path, image)
|
|
return default_storage.url(full_path)
|
|
|
|
# If `commit is False`, return the path and in-memory image.
|
|
image_data = namedtuple('image_data', ['path', 'image'])
|
|
return image_data(path=full_path, image=image)
|
|
|
|
@staticmethod
|
|
def _process_raster(image, extension):
|
|
"""
|
|
Processing of raster graphic image using Python Imaging Library (PIL).
|
|
|
|
This is where raster graphics are processed to the specifications
|
|
as defined in ``settings.py``.
|
|
|
|
*Note*: The file needs to be uploaded and saved temporarily in the
|
|
memory to enable processing tasks using Python Imaging Library (PIL)
|
|
to take place and subsequently retained until written onto the disk.
|
|
|
|
:param image: Non-SVG image as processed by Django.
|
|
:type image: django.forms.BaseForm.file
|
|
:param extension: Image extension (e.g.: png, jpg, gif)
|
|
:type extension: str
|
|
:return: The image object ready to be written into a file.
|
|
:rtype: BytesIO
|
|
"""
|
|
thumb_io = BytesIO()
|
|
preped_image = scale_and_crop(image, **MARKDOWNX_IMAGE_MAX_SIZE)
|
|
preped_image.save(thumb_io, extension)
|
|
thumb_io.seek(0, SEEK_END)
|
|
return thumb_io
|
|
|
|
@staticmethod
|
|
def get_unique_file_name(file_name):
|
|
"""
|
|
Generates a universally unique ID using Python ``UUID`` and attaches the
|
|
extension of file name to it.
|
|
|
|
:param file_name: Name of the uploaded file, including the extension.
|
|
:type file_name: str
|
|
:return: Universally unique ID, ending with the extension extracted
|
|
from ``file_name``.
|
|
:rtype: str
|
|
"""
|
|
extension = 1
|
|
extension_dot_index = 1
|
|
|
|
file_name = "{unique_name}.{extension}".format(
|
|
unique_name=uuid4(),
|
|
extension=path.splitext(file_name)[extension][extension_dot_index:]
|
|
)
|
|
return file_name
|
|
|
|
def clean(self):
|
|
"""
|
|
Checks the upload against allowed extensions and maximum size.
|
|
|
|
:return: Upload
|
|
"""
|
|
upload = self.cleaned_data.get('image')
|
|
|
|
# -----------------------------------------------
|
|
# See comments in `self._error_templates` for
|
|
# additional information on each error.
|
|
# -----------------------------------------------
|
|
if not upload:
|
|
raise MarkdownxImageUploadError.not_uploaded()
|
|
|
|
content_type = upload.content_type
|
|
file_size = upload.size
|
|
|
|
if content_type not in MARKDOWNX_UPLOAD_CONTENT_TYPES:
|
|
|
|
raise MarkdownxImageUploadError.unsupported_format()
|
|
|
|
elif file_size > MARKDOWNX_UPLOAD_MAX_SIZE:
|
|
|
|
raise MarkdownxImageUploadError.invalid_size(
|
|
current=file_size,
|
|
expected=MARKDOWNX_UPLOAD_MAX_SIZE
|
|
)
|
|
|
|
return upload
|