Repository: django-cms/django-filer

Issue:
Crash in filer when creating icons when the image doesn't exists
```python
  
File "/usr/local/lib/python3.9/site-packages/django/contrib/admin/sites.py", line 232, in inner
    return view(request, *args, **kwargs)
  File "/usr/local/lib/python3.9/site-packages/filer/admin/fileadmin.py", line 179, in icon_view
    thumbnail = thumbnailer.get_thumbnail(thumbnail_options, generate=True)
  File "/usr/local/lib/python3.9/site-packages/easy_thumbnails/files.py", line 508, in get_thumbnail
    thumbnail = self.generate_thumbnail(
  File "/usr/local/lib/python3.9/site-packages/easy_thumbnails/files.py", line 387, in generate_thumbnail
    raise exceptions.InvalidImageFormatError(msg.format(name=self.name))

Exception Type: InvalidImageFormatError at /admin/filer/image/icon/4/40
Exception Value: The source file does not appear to be an image: 'filer_public/83/6c/836cd182-d2ff-44d9-aa89-033f051179f7/header.jpg'

```

It is introduced in the recent code changes done in Filer 3.0.0


Discussion:

Resolving patch:
diff --git a/filer/admin/fileadmin.py b/filer/admin/fileadmin.py
--- a/filer/admin/fileadmin.py
+++ b/filer/admin/fileadmin.py
@@ -1,11 +1,13 @@
 from django import forms
 from django.contrib.admin.utils import unquote
+from django.contrib.staticfiles.storage import staticfiles_storage
 from django.http import Http404, HttpResponse, HttpResponseRedirect
 from django.shortcuts import get_object_or_404
 from django.urls import path, reverse
 from django.utils.safestring import mark_safe
 from django.utils.translation import gettext as _
 
+from easy_thumbnails.exceptions import InvalidImageFormatError
 from easy_thumbnails.files import get_thumbnailer
 from easy_thumbnails.options import ThumbnailOptions
 
@@ -174,10 +176,13 @@ def icon_view(self, request, file_id: int, size: int) -> HttpResponse:
         if not isinstance(file, BaseImage):
             raise Http404()
 
-        thumbnailer = get_thumbnailer(file)
-        thumbnail_options = ThumbnailOptions({'size': (size, size), "crop": True})
-        thumbnail = thumbnailer.get_thumbnail(thumbnail_options, generate=True)
-        return HttpResponseRedirect(thumbnail.url)
+        try:
+            thumbnailer = get_thumbnailer(file)
+            thumbnail_options = ThumbnailOptions({'size': (size, size), "crop": True})
+            thumbnail = thumbnailer.get_thumbnail(thumbnail_options, generate=True)
+            return HttpResponseRedirect(thumbnail.url)
+        except InvalidImageFormatError:
+            return HttpResponseRedirect(staticfiles_storage.url('filer/icons/file-missing.svg'))
 
 
 FileAdmin.fieldsets = FileAdmin.build_fieldsets()
diff --git a/filer/apps.py b/filer/apps.py
--- a/filer/apps.py
+++ b/filer/apps.py
@@ -1,3 +1,5 @@
+import mimetypes
+
 from django.apps import AppConfig
 from django.core.exceptions import ImproperlyConfigured
 from django.utils.translation import gettext_lazy as _
@@ -14,8 +16,14 @@ def register_optional_heif_supprt(self):
 
             from .settings import IMAGE_EXTENSIONS, IMAGE_MIME_TYPES
 
+            # Register with easy_thumbnails
             register_heif_opener()
-            IMAGE_EXTENSIONS += [".heic", ".heics", ".heif", ".heifs", ".hif"]
+            HEIF_EXTENSIONS = [".heic", ".heics", ".heif", ".heifs", ".hif"]
+            # Add extensions to python mimetypes which filer uses
+            for ext in HEIF_EXTENSIONS:
+                mimetypes.add_type("image/heic", ext)
+            # Mark them as images
+            IMAGE_EXTENSIONS += HEIF_EXTENSIONS
             IMAGE_MIME_TYPES.append("heic")
         except (ModuleNotFoundError, ImportError):
             # No heif support installed
@@ -52,5 +60,8 @@ def resolve_validators(self):
             self.FILE_VALIDATORS[mime_type] = functions
 
     def ready(self):
+        # Make webp mime type known to python (needed for python < 3.11)
+        mimetypes.add_type("image/webp", ".webp")
+        #
         self.resolve_validators()
         self.register_optional_heif_supprt()


Test patch:
diff --git a/tests/requirements/base.txt b/tests/requirements/base.txt
--- a/tests/requirements/base.txt
+++ b/tests/requirements/base.txt
@@ -1,5 +1,6 @@
 # requirements from setup.py
 Pillow
+pillow-heif
 django-app-helper>=3.3.1
 
 # other requirements
diff --git a/tests/test_admin.py b/tests/test_admin.py
--- a/tests/test_admin.py
+++ b/tests/test_admin.py
@@ -21,7 +21,7 @@
 from filer.models.filemodels import File
 from filer.models.foldermodels import Folder, FolderPermission
 from filer.models.virtualitems import FolderRoot
-from filer.settings import FILER_IMAGE_MODEL
+from filer.settings import DEFERRED_THUMBNAIL_SIZES, FILER_IMAGE_MODEL
 from filer.templatetags.filer_admin_tags import file_icon_url
 from filer.thumbnail_processors import normalize_subject_location
 from filer.utils.loader import load_model
@@ -254,9 +254,49 @@ class FilerImageAdminUrlsTests(TestCase):
     def setUp(self):
         self.superuser = create_superuser()
         self.client.login(username='admin', password='secret')
+        self.img = create_image()
+        self.image_name = 'test_file.jpg'
+        self.filename = os.path.join(settings.FILE_UPLOAD_TEMP_DIR, self.image_name)
+        self.img.save(self.filename, 'JPEG')
+        with open(self.filename, 'rb') as upload:
+            self.file_object = Image.objects.create(file=django.core.files.File(upload, name=self.image_name))
 
     def tearDown(self):
         self.client.logout()
+        os.remove(self.filename)
+
+    def test_icon_view_sizes(self):
+        """Tests if redirects are issued for accepted thumbnail sizes and 404 otherwise"""
+        test_set = tuple((size, 302) for size in DEFERRED_THUMBNAIL_SIZES)
+        test_set += (50, 404), (90, 404), (320, 404)
+        for size, expected_status in test_set:
+            url = reverse('admin:filer_file_fileicon', kwargs={
+                'file_id': self.file_object.pk,
+                'size': size,
+            })
+            response = self.client.get(url)
+            self.assertEqual(response.status_code, expected_status)
+            if response.status_code == 302:  # redirect
+                # Redirects to a media file
+                self.assertIn("/media/", response["Location"])
+                # Does not redirect to a static file
+                self.assertNotIn("/static/", response["Location"])
+
+    def test_missing_file(self):
+        image = Image.objects.create(
+            owner=self.superuser,
+            original_filename="some-image.jpg",
+        )
+        url = reverse('admin:filer_file_fileicon', kwargs={
+            'file_id': image.pk,
+            'size': 80,
+        })
+        # Make file unaccessible
+
+        response = self.client.get(url)
+
+        self.assertEqual(response.status_code, 302)
+        self.assertIn("icons/file-missing.svg", response["Location"])
 
 
 class FilerClipboardAdminUrlsTests(TestCase):
diff --git a/tests/test_utils.py b/tests/test_utils.py
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -1,3 +1,4 @@
+import mimetypes
 import os
 from zipfile import ZipFile
 
@@ -5,6 +6,7 @@
 from django.core.files import File as DjangoFile
 from django.test.testcases import TestCase
 
+from filer.settings import IMAGE_EXTENSIONS
 from filer.utils.loader import load_object
 from filer.utils.zip import unzip
 from tests.helpers import create_image
@@ -62,3 +64,11 @@ def tearDown(self):
     def test_unzipping_works(self):
         result = unzip(self.zipfilename)
         self.assertEqual(result[0][0].name, self.file.name)
+
+
+class MimeTypesTestCase(TestCase):
+    def test_mime_types_known(self):
+        """Ensure that for all IMAGE_EXTENSIONS the mime types can be identified"""
+        for ext in IMAGE_EXTENSIONS:
+            self.assertIsNotNone(mimetypes.guess_type(f"file{ext}")[0],
+                                 f"Mime type for extension {ext} unknown")
diff --git a/tests/test_validation.py b/tests/test_validation.py
--- a/tests/test_validation.py
+++ b/tests/test_validation.py
@@ -1,14 +1,14 @@
 import os
 
+import django.core
 from django.apps import apps
 from django.conf import settings
-import django.core
 from django.test import TestCase
 from django.urls import reverse
 from django.utils.crypto import get_random_string
 
 from filer.models import File, Folder
-from filer.validation import validate_upload, FileValidationError
+from filer.validation import FileValidationError, validate_upload
 from tests.helpers import create_superuser