mirror of
https://github.com/zulip/zulip.git
synced 2025-11-01 12:33:40 +00:00
thumbnail: Use a consistent set of supported image types.
This commit is contained in:
committed by
Tim Abbott
parent
a091b9ef81
commit
4bc563128e
@@ -13,7 +13,18 @@ export type UploadFunction = (
|
||||
|
||||
const default_max_file_size = 5;
|
||||
|
||||
const supported_types = ["image/jpeg", "image/png", "image/gif", "image/tiff"];
|
||||
// These formats do not need to be universally understood by clients; they are all
|
||||
// converted, server-side, currently to PNGs. This list should be kept in sync with
|
||||
// the THUMBNAIL_ACCEPT_IMAGE_TYPES in zerver/lib/thumbnail.py
|
||||
const supported_types = [
|
||||
"image/avif",
|
||||
"image/gif",
|
||||
"image/heic",
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/tiff",
|
||||
"image/webp",
|
||||
];
|
||||
|
||||
function is_image_format(file: File): boolean {
|
||||
const type = file.type;
|
||||
|
||||
@@ -22,6 +22,27 @@ IMAGE_BOMB_TOTAL_PIXELS = 90000000
|
||||
MAX_EMOJI_GIF_FILE_SIZE_BYTES = 128 * 1024 # 128 kb
|
||||
|
||||
|
||||
# These are the image content-types which the server supports parsing
|
||||
# and thumbnailing; these do not need to supported on all browsers,
|
||||
# since we will the serving thumbnailed versions of them. Note that
|
||||
# this does not provide any *security*, since the content-type is
|
||||
# provided by the browser, and may not match the bytes they uploaded.
|
||||
#
|
||||
# This should be kept synced with the client-side image-picker in
|
||||
# web/upload_widget.ts.
|
||||
THUMBNAIL_ACCEPT_IMAGE_TYPES = frozenset(
|
||||
[
|
||||
"image/avif",
|
||||
"image/gif",
|
||||
"image/heic",
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/tiff",
|
||||
"image/webp",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class BadImageError(JsonableError):
|
||||
code = ErrorCode.BAD_IMAGE
|
||||
|
||||
|
||||
@@ -3,17 +3,19 @@ import os
|
||||
from concurrent.futures import ProcessPoolExecutor, as_completed
|
||||
|
||||
import bmemcached
|
||||
import magic
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.db import connection
|
||||
|
||||
from zerver.lib.avatar_hash import user_avatar_path
|
||||
from zerver.lib.mime_types import guess_type
|
||||
from zerver.lib.upload import upload_avatar_image, upload_emoji_image
|
||||
from zerver.lib.upload import upload_emoji_image, write_avatar_images
|
||||
from zerver.lib.upload.s3 import S3UploadBackend, upload_image_to_s3
|
||||
from zerver.models import Attachment, RealmEmoji, UserProfile
|
||||
|
||||
s3backend = S3UploadBackend()
|
||||
mime_magic = magic.Magic(mime=True)
|
||||
|
||||
|
||||
def transfer_uploads_to_s3(processes: int) -> None:
|
||||
@@ -30,7 +32,18 @@ def _transfer_avatar_to_s3(user: UserProfile) -> None:
|
||||
file_path = os.path.join(settings.LOCAL_AVATARS_DIR, avatar_path)
|
||||
try:
|
||||
with open(file_path + ".original", "rb") as f:
|
||||
upload_avatar_image(f, user, backend=s3backend, future=False)
|
||||
# We call write_avatar_images directly to walk around the
|
||||
# content-type checking in upload_avatar_image. We don't
|
||||
# know the original file format, and we don't need to know
|
||||
# it because we never serve them directly.
|
||||
write_avatar_images(
|
||||
user_avatar_path(user, future=False),
|
||||
user,
|
||||
f.read(),
|
||||
content_type="application/octet-stream",
|
||||
backend=s3backend,
|
||||
future=False,
|
||||
)
|
||||
logging.info("Uploaded avatar for %s in realm %s", user.id, user.realm.name)
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
@@ -18,6 +18,7 @@ from zerver.lib.outgoing_http import OutgoingSession
|
||||
from zerver.lib.thumbnail import (
|
||||
MAX_EMOJI_GIF_FILE_SIZE_BYTES,
|
||||
MEDIUM_AVATAR_SIZE,
|
||||
THUMBNAIL_ACCEPT_IMAGE_TYPES,
|
||||
BadImageError,
|
||||
resize_avatar,
|
||||
resize_emoji,
|
||||
@@ -250,6 +251,8 @@ def upload_avatar_image(
|
||||
) -> None:
|
||||
if content_type is None:
|
||||
content_type = guess_type(user_file.name)[0]
|
||||
if content_type not in THUMBNAIL_ACCEPT_IMAGE_TYPES:
|
||||
raise BadImageError(_("Invalid image format"))
|
||||
file_path = user_avatar_path(user_profile, future=future)
|
||||
|
||||
image_data = user_file.read()
|
||||
@@ -311,12 +314,16 @@ def delete_avatar_image(user_profile: UserProfile, avatar_version: int) -> None:
|
||||
|
||||
|
||||
def upload_icon_image(user_file: IO[bytes], user_profile: UserProfile, content_type: str) -> None:
|
||||
if content_type not in THUMBNAIL_ACCEPT_IMAGE_TYPES:
|
||||
raise BadImageError(_("Invalid image format"))
|
||||
upload_backend.upload_realm_icon_image(user_file, user_profile, content_type)
|
||||
|
||||
|
||||
def upload_logo_image(
|
||||
user_file: IO[bytes], user_profile: UserProfile, night: bool, content_type: str
|
||||
) -> None:
|
||||
if content_type not in THUMBNAIL_ACCEPT_IMAGE_TYPES:
|
||||
raise BadImageError(_("Invalid image format"))
|
||||
upload_backend.upload_realm_logo_image(user_file, user_profile, night, content_type)
|
||||
|
||||
|
||||
|
||||
BIN
zerver/tests/images/unsupported.bmp
Normal file
BIN
zerver/tests/images/unsupported.bmp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
@@ -1058,7 +1058,6 @@ class AvatarTest(UploadSerializeMixin, ZulipTestCase):
|
||||
("img.tif", "tif_resized.png"),
|
||||
("cmyk.jpg", None),
|
||||
]
|
||||
corrupt_files = ["text.txt", "corrupt.png", "corrupt.gif"]
|
||||
|
||||
def test_get_gravatar_avatar(self) -> None:
|
||||
self.login("hamlet")
|
||||
@@ -1341,15 +1340,24 @@ class AvatarTest(UploadSerializeMixin, ZulipTestCase):
|
||||
"""
|
||||
A PUT request to /json/users/me/avatar with an invalid file should fail.
|
||||
"""
|
||||
for fname in self.corrupt_files:
|
||||
corrupt_files = [
|
||||
("text.txt", False),
|
||||
("unsupported.bmp", False),
|
||||
("corrupt.png", True),
|
||||
("corrupt.gif", True),
|
||||
]
|
||||
for fname, is_valid_image_format in corrupt_files:
|
||||
with self.subTest(fname=fname):
|
||||
self.login("hamlet")
|
||||
with get_test_image_file(fname) as fp:
|
||||
result = self.client_post("/json/users/me/avatar", {"file": fp})
|
||||
|
||||
self.assert_json_error(
|
||||
result, "Could not decode image; did you upload an image file?"
|
||||
)
|
||||
if is_valid_image_format:
|
||||
self.assert_json_error(
|
||||
result, "Could not decode image; did you upload an image file?"
|
||||
)
|
||||
else:
|
||||
self.assert_json_error(result, "Invalid image format")
|
||||
user_profile = self.example_user("hamlet")
|
||||
self.assertEqual(user_profile.avatar_version, 1)
|
||||
|
||||
@@ -1490,7 +1498,6 @@ class RealmIconTest(UploadSerializeMixin, ZulipTestCase):
|
||||
("img.tif", "tif_resized.png"),
|
||||
("cmyk.jpg", None),
|
||||
]
|
||||
corrupt_files = ["text.txt", "corrupt.png", "corrupt.gif"]
|
||||
|
||||
def test_no_admin_user_upload(self) -> None:
|
||||
self.login("hamlet")
|
||||
@@ -1558,15 +1565,24 @@ class RealmIconTest(UploadSerializeMixin, ZulipTestCase):
|
||||
"""
|
||||
A PUT request to /json/realm/icon with an invalid file should fail.
|
||||
"""
|
||||
for fname in self.corrupt_files:
|
||||
corrupt_files = [
|
||||
("text.txt", False),
|
||||
("unsupported.bmp", False),
|
||||
("corrupt.png", True),
|
||||
("corrupt.gif", True),
|
||||
]
|
||||
for fname, is_valid_image_format in corrupt_files:
|
||||
with self.subTest(fname=fname):
|
||||
self.login("iago")
|
||||
with get_test_image_file(fname) as fp:
|
||||
result = self.client_post("/json/realm/icon", {"file": fp})
|
||||
|
||||
self.assert_json_error(
|
||||
result, "Could not decode image; did you upload an image file?"
|
||||
)
|
||||
if is_valid_image_format:
|
||||
self.assert_json_error(
|
||||
result, "Could not decode image; did you upload an image file?"
|
||||
)
|
||||
else:
|
||||
self.assert_json_error(result, "Invalid image format")
|
||||
|
||||
def test_delete_icon(self) -> None:
|
||||
"""
|
||||
@@ -1634,7 +1650,6 @@ class RealmLogoTest(UploadSerializeMixin, ZulipTestCase):
|
||||
("img.tif", "tif_resized.png"),
|
||||
("cmyk.jpg", None),
|
||||
]
|
||||
corrupt_files = ["text.txt", "corrupt.png", "corrupt.gif"]
|
||||
|
||||
def test_no_admin_user_upload(self) -> None:
|
||||
self.login("hamlet")
|
||||
@@ -1744,7 +1759,13 @@ class RealmLogoTest(UploadSerializeMixin, ZulipTestCase):
|
||||
"""
|
||||
A PUT request to /json/realm/logo with an invalid file should fail.
|
||||
"""
|
||||
for fname in self.corrupt_files:
|
||||
corrupt_files = [
|
||||
("text.txt", False),
|
||||
("unsupported.bmp", False),
|
||||
("corrupt.png", True),
|
||||
("corrupt.gif", True),
|
||||
]
|
||||
for fname, is_valid_image_format in corrupt_files:
|
||||
with self.subTest(fname=fname):
|
||||
self.login("iago")
|
||||
with get_test_image_file(fname) as fp:
|
||||
@@ -1752,9 +1773,12 @@ class RealmLogoTest(UploadSerializeMixin, ZulipTestCase):
|
||||
"/json/realm/logo", {"file": fp, "night": orjson.dumps(self.night).decode()}
|
||||
)
|
||||
|
||||
self.assert_json_error(
|
||||
result, "Could not decode image; did you upload an image file?"
|
||||
)
|
||||
if is_valid_image_format:
|
||||
self.assert_json_error(
|
||||
result, "Could not decode image; did you upload an image file?"
|
||||
)
|
||||
else:
|
||||
self.assert_json_error(result, "Invalid image format")
|
||||
|
||||
def test_delete_logo(self) -> None:
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user