Skip to content

Commit 5e67644

Browse files
Merge pull request #4961 from open-formulieren/bug/4795-msg-file-upload
.msg file upload
2 parents 28754fd + f3bf841 commit 5e67644

File tree

9 files changed

+130
-31
lines changed

9 files changed

+130
-31
lines changed

src/openforms/conf/locale/nl/LC_MESSAGES/django.po

+1-1
Original file line numberDiff line numberDiff line change
@@ -5384,7 +5384,7 @@ msgid ""
53845384
"extension."
53855385
msgstr ""
53865386
"Het bestandstype kon niet bepaald worden. Controleer of de bestandsnaam met "
5387-
"een extensie eindigt (bijvoorbeel '.pdf' of '.png')."
5387+
"een extensie eindigt (bijvoorbeeld '.pdf' of '.png')."
53885388

53895389
#: openforms/formio/components/vanilla.py:365
53905390
#, python-brace-format

src/openforms/formio/api/validators.py

+26-15
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
from pathlib import Path
23
from typing import Iterable
34

45
from django.core.files.uploadedfile import UploadedFile
@@ -56,29 +57,40 @@ def __init__(self, allowed_mime_types: Iterable[str] | None = None):
5657

5758
def __call__(self, value: UploadedFile) -> None:
5859
head = value.read(2048)
59-
ext = value.name.split(".")[-1]
60-
mime_type = magic.from_buffer(head, mime=True)
60+
ext = Path(value.name or "").suffix[1:]
61+
detected_mime_type = magic.from_buffer(head, mime=True)
62+
provided_mime_type = value.content_type or "application/octet-stream"
6163

6264
# gh #2520
6365
# application/x-ole-storage on Arch with shared-mime-info 2.0+155+gf4e7cbc-1
64-
if mime_type in ["application/CDFV2", "application/x-ole-storage"]:
66+
if detected_mime_type in ["application/CDFV2", "application/x-ole-storage"]:
6567
whole_file = head + value.read()
66-
mime_type = magic.from_buffer(whole_file, mime=True)
68+
detected_mime_type = magic.from_buffer(whole_file, mime=True)
6769

68-
if mime_type == "image/heif":
69-
mime_type = "image/heic"
70+
if detected_mime_type == "image/heif":
71+
detected_mime_type = "image/heic"
7072

7173
if not (
7274
self.any_allowed
73-
or mimetype_allowed(mime_type, self._regular_mimes, self._wildcard_mimes)
75+
or mimetype_allowed(
76+
detected_mime_type, self._regular_mimes, self._wildcard_mimes
77+
)
7478
):
7579
raise serializers.ValidationError(
7680
_("The provided file is not a valid file type.")
7781
)
7882

83+
if not ext:
84+
raise serializers.ValidationError(
85+
_(
86+
"Could not determine the file type. Please make sure the file name "
87+
"has an extension."
88+
)
89+
)
90+
7991
# Contents is allowed. Do extension or submitted content_type agree?
80-
if value.content_type == "application/octet-stream":
81-
m = magic.Magic(extension=True)
92+
if provided_mime_type == "application/octet-stream":
93+
m = magic.Magic(extension=True) # pyright: ignore[reportCallIssue]
8294
extensions = m.from_buffer(head).split("/")
8395
# magic db doesn't know any more specific extension(s), so accept the
8496
# file
@@ -101,27 +113,26 @@ def __call__(self, value: UploadedFile) -> None:
101113
# If the file does not strictly follow the conventions of CSV (e.g. non-standard delimiters),
102114
# may not be considered as a valid CSV.
103115
elif (
104-
value.content_type == "text/csv"
105-
and mime_type == "text/plain"
116+
provided_mime_type == "text/csv"
117+
and detected_mime_type == "text/plain"
106118
and ext == "csv"
107119
):
108120
return
109-
elif mime_type == "image/heic" and value.content_type in (
121+
elif detected_mime_type == "image/heic" and provided_mime_type in (
110122
"image/heic",
111123
"image/heif",
112124
):
113125
return
114-
115126
# gh #4658
116127
# Windows use application/x-zip-compressed as a mimetype for .zip files, which
117128
# is deprecated but still we need to support it. Instead, the common case for
118129
# zip files is application/zip or application/zip-compressed mimetype.
119-
elif mime_type == "application/zip" and value.content_type in (
130+
elif detected_mime_type == "application/zip" and provided_mime_type in (
120131
"application/zip-compressed",
121132
"application/x-zip-compressed",
122133
):
123134
return
124-
elif mime_type != value.content_type:
135+
elif provided_mime_type != detected_mime_type:
125136
raise serializers.ValidationError(
126137
_("The provided file is not a {file_type}.").format(
127138
filename=value.name, file_type=f".{ext}"

src/openforms/formio/components/vanilla.py

+1-8
Original file line numberDiff line numberDiff line change
@@ -338,14 +338,7 @@ class FileSerializer(serializers.Serializer):
338338
originalName = serializers.CharField(trim_whitespace=False)
339339
size = serializers.IntegerField(min_value=0)
340340
storage = serializers.ChoiceField(choices=["url"])
341-
type = serializers.CharField(
342-
error_messages={
343-
"blank": _(
344-
"Could not determine the file type. Please make sure the file name "
345-
"has an extension."
346-
),
347-
}
348-
)
341+
type = serializers.CharField(required=True, allow_blank=True)
349342
url = serializers.URLField()
350343
data = FileDataSerializer() # type: ignore
351344

11 KB
Binary file not shown.

src/openforms/formio/tests/test_validators.py

+34
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,23 @@ def test_mime_type_inferred_from_magic(self):
108108
except ValidationError as e:
109109
self.fail(f"Valid file failed validation: {e}")
110110

111+
def test_unknown_file_type(self):
112+
file = SimpleUploadedFile(
113+
"unknown-type",
114+
b"test",
115+
content_type="application/octet-stream", # see e2e test SingleFileTests.test_unknown_file_type
116+
)
117+
validator = validators.MimeTypeValidator(
118+
allowed_mime_types=None
119+
) # allows any mime type
120+
121+
with self.assertRaisesMessage(
122+
ValidationError,
123+
"Could not determine the file type. Please make sure the file name "
124+
"has an extension.",
125+
):
126+
validator(file)
127+
111128
def test_star_wildcard_in_allowed_mimetypes(self):
112129
validator = validators.MimeTypeValidator({"*"})
113130

@@ -202,6 +219,23 @@ def test_allowed_mime_types_for_csv_files(self):
202219

203220
validator(sample)
204221

222+
def test_allowed_mime_types_for_msg_files(self):
223+
valid_type = "application/vnd.ms-outlook"
224+
msg_file = TEST_FILES / "test.msg"
225+
validator = validators.MimeTypeValidator(allowed_mime_types=[valid_type])
226+
227+
# 4795
228+
# The sdk cannot determine the content_type for .msg files correctly.
229+
# Because .msg is a windows specific file, and linux and MacOS don't know it.
230+
# So we simulate the scenario where content_type is unknown
231+
sample = SimpleUploadedFile(
232+
name="test.msg",
233+
content=msg_file.read_bytes(),
234+
content_type="", # replicate the behaviour of the frontend
235+
)
236+
237+
validator(sample)
238+
205239
def test_validate_files_multiple_mime_types(self):
206240
"""Assert that validation of files associated with multiple mime types works
207241

src/openforms/formio/tests/validation/test_file.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -602,10 +602,10 @@ def test_attach_upload_validates_unknown_file_type(self):
602602
}
603603

604604
is_valid, errors = validate_formio_data(component, data, submission=submission)
605-
error = extract_error(errors["foo"][0], "type")
605+
error = extract_error(errors["foo"][0], "non_field_errors")
606606

607607
self.assertFalse(is_valid)
608-
self.assertEqual(error.code, "blank")
608+
self.assertEqual(error.code, "invalid")
609609
self.assertEqual(
610610
error,
611611
_(

src/openforms/tests/e2e/data/test.msg

11 KB
Binary file not shown.

src/openforms/tests/e2e/test_file_upload.py

+61
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,64 @@ def setUpTestData():
7777
await expect(
7878
page.get_by_text("Een moment geduld", exact=False)
7979
).to_be_visible()
80+
81+
async def test_form_with_msg_file_upload(self):
82+
# If using the ci.py settings locally, the SDK_RELEASE variable should be set to 'latest', otherwise the
83+
# JS/CSS for the SDK will not be found (since they will be expected to be in the folder
84+
# openforms/static/sdk/<SDK version tag> instead of openforms/static/sdk
85+
@sync_to_async
86+
def setUpTestData():
87+
# set up a form
88+
form = FormFactory.create(
89+
name="Form with file upload",
90+
slug="form-with-file-upload",
91+
generate_minimal_setup=True,
92+
formstep__form_definition__name="First step",
93+
formstep__form_definition__slug="first-step",
94+
formstep__form_definition__configuration={
95+
"components": [
96+
{
97+
"type": "file",
98+
"key": "fileUpload",
99+
"label": "File Upload",
100+
"storage": "url",
101+
"validate": {
102+
"required": True,
103+
},
104+
}
105+
]
106+
},
107+
translation_enabled=False, # force Dutch
108+
ask_privacy_consent=False,
109+
ask_statement_of_truth=False,
110+
)
111+
return form
112+
113+
form = await setUpTestData()
114+
form_url = str(
115+
furl(self.live_server_url)
116+
/ reverse("forms:form-detail", kwargs={"slug": form.slug})
117+
)
118+
119+
with patch("openforms.utils.validators.allow_redirect_url", return_value=True):
120+
async with browser_page() as page:
121+
await page.goto(form_url)
122+
123+
await page.get_by_role("button", name="Formulier starten").click()
124+
125+
async with page.expect_file_chooser() as fc_info:
126+
await page.get_by_text("blader").click()
127+
128+
file_chooser = await fc_info.value
129+
await file_chooser.set_files(TEST_FILES / "test.msg")
130+
131+
await page.wait_for_load_state("networkidle")
132+
133+
uploaded_file = page.get_by_role("link", name="test.msg")
134+
await expect(uploaded_file).to_be_visible()
135+
136+
await page.get_by_role("button", name="Volgende").click()
137+
await page.get_by_role("button", name="Verzenden").click()
138+
await expect(
139+
page.get_by_text("Een moment geduld", exact=False)
140+
).to_be_visible()

src/openforms/tests/e2e/test_input_validation.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -939,8 +939,8 @@ def test_unknown_file_type(self):
939939

940940
# The frontend validation will *not* create a TemporaryFileUpload,
941941
# as the frontend will block the upload because of the invalid file type.
942-
# However the user could do an handcrafted API call.
943-
# For this reason, we manually create an invalid TemporaryFileUpload
942+
# However the user could do a handcrafted API call.
943+
# For this reason, we manually try to create an invalid TemporaryFileUpload
944944
# and use it for the `api_value`:
945945

946946
with open(TEST_FILES / "unknown-type", "rb") as infile:
@@ -955,7 +955,7 @@ def test_unknown_file_type(self):
955955
ui_files=[TEST_FILES / "unknown-type"],
956956
expected_ui_error=(
957957
"Het bestandstype kon niet bepaald worden. Controleer of de "
958-
"bestandsnaam met een extensie eindigt (bijvoorbeel '.pdf' of "
958+
"bestandsnaam met een extensie eindigt (bijvoorbeeld '.pdf' of "
959959
"'.png')."
960960
),
961961
api_value=[
@@ -966,8 +966,8 @@ def test_unknown_file_type(self):
966966
],
967967
)
968968

969-
# Make sure the frontend did not create one:
970-
self.assertEqual(TemporaryFileUpload.objects.count(), 1)
969+
# Make sure that no temporary files were created
970+
self.assertEqual(TemporaryFileUpload.objects.count(), 0)
971971

972972

973973
class SingleAddressNLTests(ValidationsTestCase):

0 commit comments

Comments
 (0)