Skip to content

Commit 5473f09

Browse files
authored
Restrict the accepted file types in the browser (#273)
* Restrict the accepted file types in the browser Previously the `FileInput` widget allowed all file types, which meant the user could select *any* file and then only find out it's not accepted after uploading. This ensures the `FileInput` only accepts the listed file extensions _or_ in the absence of file extensions, restricts to audio/video files as appropriate. * Added link to MDN documentation on the `accept` attribute
1 parent 9fe2bd8 commit 5473f09

File tree

2 files changed

+82
-1
lines changed

2 files changed

+82
-1
lines changed

src/wagtailmedia/forms.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
)
1111
from wagtail.models import Collection
1212

13-
from wagtailmedia.models import Media
13+
from wagtailmedia.models import Media, MediaType
1414
from wagtailmedia.permissions import permission_policy as media_permission_policy
1515
from wagtailmedia.settings import wagtailmedia_settings
1616

@@ -29,6 +29,14 @@ def formfield_for_dbfield(db_field, **kwargs):
2929
return db_field.formfield(**kwargs)
3030

3131

32+
def format_extensions_for_accept_value(allowed_extensions: list[str]) -> str:
33+
"""Returns the specified extensions in a format usable in the `accept=""` attribute of a `FileInput` widget.
34+
This assumes the list of extensions are the bare extensions e.g., `["mp4", "webm"]` and prefixes each extension with
35+
a ".".
36+
"""
37+
return ",".join([f".{e}" for e in allowed_extensions])
38+
39+
3240
class BaseMediaForm(BaseCollectionMemberForm):
3341
class Meta:
3442
widgets = {
@@ -42,12 +50,41 @@ class Meta:
4250
def __init__(self, *args, **kwargs):
4351
super().__init__(*args, **kwargs)
4452

53+
if file_accept_value := self.get_file_accept_value(self.instance.type):
54+
self.fields["file"].widget.attrs["accept"] = file_accept_value
55+
4556
if self.instance.type == "audio":
4657
for name in ("width", "height"):
4758
# these fields might be editable=False so verify before accessing
4859
if name in self.fields:
4960
del self.fields[name]
5061

62+
@staticmethod
63+
def get_file_accept_value(media_type: MediaType) -> str | None:
64+
"""Dynamically set the `accept` attribute on the file input based on the media type. If allowed extensions have
65+
been configured in settings, this will restrict the file input to only those extensions. Otherwise, it will
66+
fall back to allowing all video or audio file types.
67+
See https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/file#unique_file_type_specifiers
68+
for more information on the `accept` attribute.
69+
"""
70+
71+
if media_type == MediaType.VIDEO:
72+
return (
73+
format_extensions_for_accept_value(
74+
wagtailmedia_settings.VIDEO_EXTENSIONS
75+
)
76+
or "video/*"
77+
)
78+
elif media_type == MediaType.AUDIO:
79+
return (
80+
format_extensions_for_accept_value(
81+
wagtailmedia_settings.AUDIO_EXTENSIONS
82+
)
83+
or "audio/*"
84+
)
85+
86+
return None
87+
5188

5289
def get_media_base_form():
5390
base_form_override = wagtailmedia_settings.MEDIA_FORM_BASE

tests/test_form.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from django.test import TestCase, override_settings
2+
3+
from wagtailmedia.forms import format_extensions_for_accept_value, get_media_form
4+
from wagtailmedia.models import MediaType, get_media_model
5+
from wagtailmedia.settings import wagtailmedia_settings
6+
7+
8+
class TestForm(TestCase):
9+
def test_file_input_accept_attribute(self):
10+
"""Tests that the file input widget only accepts the default file extensions per media type."""
11+
Media = get_media_model()
12+
MediaForm = get_media_form(Media)
13+
media_type_to_extensions = {
14+
MediaType.VIDEO: wagtailmedia_settings.VIDEO_EXTENSIONS,
15+
MediaType.AUDIO: wagtailmedia_settings.AUDIO_EXTENSIONS,
16+
}
17+
for media_type, extensions in media_type_to_extensions.items():
18+
media = Media(type=media_type)
19+
form = MediaForm(instance=media)
20+
21+
self.assertIn(
22+
f'accept="{format_extensions_for_accept_value(extensions)}"',
23+
form["file"].as_widget(),
24+
)
25+
26+
@override_settings(WAGTAILMEDIA={"VIDEO_EXTENSIONS": [], "AUDIO_EXTENSIONS": []})
27+
def test_file_input_accept_attribute_all_extensions_allowed(self):
28+
"""Tests that if `VIDEO_EXTENSIONS` and `AUDIO_EXTENSIONS` are set to empty lists, that the accept attribute
29+
allows all file extensions for the given media type.
30+
"""
31+
Media = get_media_model()
32+
MediaForm = get_media_form(Media)
33+
media_type_to_extensions = {
34+
MediaType.VIDEO: "video/*",
35+
MediaType.AUDIO: "audio/*",
36+
}
37+
for media_type, accept_value in media_type_to_extensions.items():
38+
media = Media(type=media_type)
39+
form = MediaForm(instance=media)
40+
41+
self.assertIn(
42+
f'accept="{accept_value}"',
43+
form["file"].as_widget(),
44+
)

0 commit comments

Comments
 (0)