fix: preserve file preview content type (#37211)

Co-authored-by: FFXN <31929997+FFXN@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Myshkin451 2026-08-14 07:45:21 +00:00 committed by GitHub
parent 0b36a0bbc0
commit c903e93e8b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 102 additions and 5 deletions

View File

@ -1,3 +1,4 @@
import os
from urllib.parse import quote
from uuid import UUID
@ -29,6 +30,18 @@ class FilePreviewQuery(FileSignatureQuery):
register_schema_models(files_ns, FileSignatureQuery, FilePreviewQuery)
def _is_svg_content(mime_type: str | None, filename: str | None, extension: str | None) -> bool:
normalized_mime_type = mime_type.split(";", 1)[0].strip().lower() if mime_type else ""
if normalized_mime_type == "image/svg+xml":
return True
normalized_extension = extension.lstrip(".").lower() if extension else ""
if normalized_extension == "svg":
return True
return bool(filename and os.path.splitext(filename)[1].lstrip(".").lower() == "svg")
@files_ns.route("/<uuid:file_id>/image-preview")
class ImagePreviewApi(Resource):
"""Deprecated endpoint for retrieving image previews."""
@ -129,10 +142,13 @@ class FilePreviewApi(Resource):
response.headers["Accept-Ranges"] = "bytes"
if upload_file.size > 0:
response.headers["Content-Length"] = str(upload_file.size)
if args.as_attachment:
is_svg = _is_svg_content(upload_file.mime_type, upload_file.name, upload_file.extension)
if args.as_attachment or is_svg:
encoded_filename = quote(upload_file.name)
response.headers["Content-Disposition"] = f"attachment; filename*=UTF-8''{encoded_filename}"
response.headers["Content-Type"] = "application/octet-stream"
response.headers["Content-Type"] = "application/octet-stream"
if is_svg:
response.headers["X-Content-Type-Options"] = "nosniff"
enforce_download_for_html(
response,

View File

@ -79,7 +79,7 @@ class TestImagePreviewApi:
class TestFilePreviewApi:
@patch.object(module, "enforce_download_for_html")
@patch.object(module, "FileService")
def test_basic_stream(self, mock_file_service, mock_enforce):
def test_inline_preview_uses_upload_file_mimetype(self, mock_file_service, mock_enforce):
module.request = fake_request(
{
"timestamp": "123",
@ -90,7 +90,12 @@ class TestFilePreviewApi:
)
generator = iter([b"data"])
upload_file = DummyUploadFile(size=100)
upload_file = DummyUploadFile(
mime_type="application/pdf",
size=100,
name="doc.pdf",
extension="pdf",
)
mock_file_service.return_value.get_file_generator_by_file_id.return_value = (
generator,
@ -102,11 +107,87 @@ class TestFilePreviewApi:
response = get_fn("file-id")
assert response.mimetype == "application/octet-stream"
assert response.mimetype == "application/pdf"
assert response.headers["Content-Type"] == "application/pdf"
assert response.headers["Content-Length"] == "100"
assert "Accept-Ranges" not in response.headers
mock_enforce.assert_called_once()
@pytest.mark.parametrize(
("mime_type", "name", "extension"),
[
("Image/SVG+XML; charset=UTF-8", "image.png", "png"),
("image/png", "image.SVG", "png"),
("image/png", "image.png", ".SVG"),
],
ids=("mime-type", "filename", "extension"),
)
@patch.object(module, "FileService")
def test_svg_preview_forces_download(self, mock_file_service, mime_type, name, extension):
module.request = fake_request(
{
"timestamp": "123",
"nonce": "abc",
"sign": "sig",
"as_attachment": False,
}
)
generator = iter([b"<svg></svg>"])
upload_file = DummyUploadFile(
mime_type=mime_type,
size=11,
name=name,
extension=extension,
)
mock_file_service.return_value.get_file_generator_by_file_id.return_value = (
generator,
upload_file,
)
api = module.FilePreviewApi()
get_fn = unwrap(api.get)
response = get_fn("file-id")
assert response.headers["Content-Disposition"].startswith("attachment")
assert response.headers["Content-Type"] == "application/octet-stream"
assert response.headers["X-Content-Type-Options"] == "nosniff"
@patch.object(module, "FileService")
def test_html_preview_still_forces_download(self, mock_file_service):
module.request = fake_request(
{
"timestamp": "123",
"nonce": "abc",
"sign": "sig",
"as_attachment": False,
}
)
generator = iter([b"<script>alert(1)</script>"])
upload_file = DummyUploadFile(
mime_type="text/html",
size=25,
name="unsafe.html",
extension="html",
)
mock_file_service.return_value.get_file_generator_by_file_id.return_value = (
generator,
upload_file,
)
api = module.FilePreviewApi()
get_fn = unwrap(api.get)
response = get_fn("file-id")
assert response.headers["Content-Disposition"].startswith("attachment")
assert response.headers["Content-Type"] == "application/octet-stream"
assert response.headers["X-Content-Type-Options"] == "nosniff"
@patch.object(module, "enforce_download_for_html")
@patch.object(module, "FileService")
def test_as_attachment(self, mock_file_service, mock_enforce):