Skip to content

Commit 3fa26d4

Browse files
committed
Mime type validation
1 parent b74d5fe commit 3fa26d4

File tree

5 files changed

+122
-16
lines changed

5 files changed

+122
-16
lines changed

src/FormToolkit/Error.elm

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ type Error id
2727
| PatternError (Maybe id)
2828
| EmailInvalid (Maybe id)
2929
| UrlInvalid (Maybe id)
30+
| MimeTypeInvalid (Maybe id) { mime : String, accepted : List String }
3031
| IsGroupNotInput (Maybe id)
3132
| NoOptionsProvided (Maybe id)
3233
| InvalidValue (Maybe id)
@@ -74,6 +75,9 @@ toEnglish error =
7475
UrlInvalid _ ->
7576
"Please enter a valid URL"
7677

78+
MimeTypeInvalid _ data ->
79+
"File type '" ++ data.mime ++ "' is not accepted. Accepted types: " ++ String.join ", " data.accepted
80+
7781
IsGroupNotInput _ ->
7882
"A group cannot have a value but the decoder is attempting to read the value"
7983

src/FormToolkit/Field.elm

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ module FormToolkit.Field exposing
1111
, selectionStart, selectionEnd
1212
, options, stringOptions, min, max, step, autogrow
1313
, class, classList
14-
, disabled, hidden, noattr, pattern
14+
, disabled, hidden, noattr, pattern, accept
1515
, copies, repeatableMin, repeatableMax
1616
, updateAttribute, updateAttributes, updateWithId
1717
, updateValuesFromJson
@@ -45,7 +45,7 @@ their attributes, update, and render them.
4545
@docs selectionStart, selectionEnd
4646
@docs options, stringOptions, min, max, step, autogrow
4747
@docs class, classList
48-
@docs disabled, hidden, noattr, pattern
48+
@docs disabled, hidden, noattr, pattern, accept
4949
5050
5151
# Groups
@@ -587,6 +587,7 @@ initAttributes fieldType =
587587
, disabled = False
588588
, hidden = False
589589
, pattern = []
590+
, acceptedMimeTypes = []
590591
}
591592

592593

@@ -913,6 +914,27 @@ pattern patternString =
913914
)
914915

915916

917+
{-| Sets accepted MIME types for file input. Supports wildcards like "image/*" or "*/*".
918+
919+
file
920+
[ label "Profile Picture"
921+
, accept [ "image/png", "image/jpeg", "image/gif" ]
922+
]
923+
924+
file
925+
[ label "Any Image"
926+
, accept [ "image/*" ]
927+
]
928+
929+
-}
930+
accept : List String -> Attribute id val
931+
accept mimeTypes =
932+
Attribute
933+
(\field ->
934+
{ field | acceptedMimeTypes = mimeTypes }
935+
)
936+
937+
916938
combineAttrs : Attribute id val -> Attribute id val -> Attribute id val
917939
combineAttrs (Attribute a) (Attribute b) =
918940
Attribute (b >> a)
@@ -1343,6 +1365,7 @@ validateNode node =
13431365
, checkRequired
13441366
, ifNotRequired checkInRange
13451367
, ifNotRequired checkEmail
1368+
, ifNotRequired checkMimeType
13461369
, ifNotRequired checkPattern
13471370
]
13481371

@@ -1467,6 +1490,68 @@ validateUrl urlString =
14671490
|> Url.fromString
14681491

14691492

1493+
checkMimeType : Node id -> Node id
1494+
checkMimeType node =
1495+
let
1496+
attrs =
1497+
Tree.value node
1498+
in
1499+
case ( attrs.fieldType, attrs.acceptedMimeTypes ) of
1500+
( File, [] ) ->
1501+
node
1502+
1503+
( File, accepted ) ->
1504+
case Internal.Value.toFile attrs.value of
1505+
Just fileValue ->
1506+
let
1507+
fileMime =
1508+
File.mime fileValue
1509+
in
1510+
if isMimeTypeAccepted fileMime accepted then
1511+
node
1512+
1513+
else
1514+
setError
1515+
(\id ->
1516+
MimeTypeInvalid id
1517+
{ mime = fileMime
1518+
, accepted = accepted
1519+
}
1520+
)
1521+
node
1522+
1523+
Nothing ->
1524+
node
1525+
1526+
_ ->
1527+
node
1528+
1529+
1530+
isMimeTypeAccepted : String -> List String -> Bool
1531+
isMimeTypeAccepted fileMime acceptedTypes =
1532+
List.any (matchesMimeType fileMime) acceptedTypes
1533+
1534+
1535+
matchesMimeType : String -> String -> Bool
1536+
matchesMimeType fileMime mimePattern =
1537+
if mimePattern == "*/*" || mimePattern == "*" then
1538+
True
1539+
1540+
else
1541+
case String.split "/" mimePattern of
1542+
[ typePattern, subTypePattern ] ->
1543+
case String.split "/" fileMime of
1544+
[ fileType, fileSubType ] ->
1545+
(typePattern == "*" || typePattern == fileType)
1546+
&& (subTypePattern == "*" || subTypePattern == fileSubType)
1547+
1548+
_ ->
1549+
False
1550+
1551+
_ ->
1552+
fileMime == mimePattern
1553+
1554+
14701555
checkPattern : Node id -> Node id
14711556
checkPattern node =
14721557
let
@@ -1573,6 +1658,9 @@ mapError transformId error =
15731658
UrlInvalid id ->
15741659
UrlInvalid (Maybe.map transformId id)
15751660

1661+
MimeTypeInvalid id data ->
1662+
MimeTypeInvalid (Maybe.map transformId id) data
1663+
15761664
ParseError id ->
15771665
ParseError (Maybe.map transformId id)
15781666

src/Internal/Field.elm

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ type alias Attributes id fieldType value status err =
6666
, disabled : Bool
6767
, hidden : Bool
6868
, pattern : List Internal.Utils.MaskToken
69+
, acceptedMimeTypes : List String
6970
}
7071

7172

@@ -103,6 +104,7 @@ mapAttributes func errMapper typeMapper valueMapper statusMapper input =
103104
, disabled = input.disabled
104105
, hidden = input.hidden
105106
, pattern = input.pattern
107+
, acceptedMimeTypes = input.acceptedMimeTypes
106108
}
107109

108110

src/Internal/Value.elm

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,12 @@ encode value =
157157
Boolean b ->
158158
Encode.bool b
159159

160-
FileValue _ ->
161-
Debug.todo "crash"
160+
FileValue fileValue ->
161+
Encode.object
162+
[ ( "name", Encode.string (File.name fileValue) )
163+
, ( "size", Encode.int (File.size fileValue) )
164+
, ( "mime", Encode.string (File.mime fileValue) )
165+
]
162166

163167
Blank ->
164168
Encode.null

src/Internal/View.elm

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1082,18 +1082,26 @@ fileInputToHtml view element =
10821082
++ element
10831083
)
10841084
[ Html.input
1085-
[ Attributes.type_ "file"
1086-
, Attributes.id (inputId view.root view.path)
1087-
, Attributes.required attrs.isRequired
1088-
, Attributes.disabled attrs.disabled
1089-
, Attributes.placeholder (Maybe.withDefault "" attrs.placeholder)
1090-
, nameAttribute view.root
1091-
, ariaDescribedByAttribute view.root view.path
1092-
, ariaInvalidAttribute view.root
1093-
, onFileChange
1094-
, Events.onFocus (view.onFocus attrs.identifier view.path)
1095-
, Events.onBlur (view.onBlur attrs.identifier view.path)
1096-
]
1085+
(List.concat
1086+
[ [ Attributes.type_ "file"
1087+
, Attributes.id (inputId view.root view.path)
1088+
, Attributes.required attrs.isRequired
1089+
, Attributes.disabled attrs.disabled
1090+
, Attributes.placeholder (Maybe.withDefault "" attrs.placeholder)
1091+
, nameAttribute view.root
1092+
, ariaDescribedByAttribute view.root view.path
1093+
, ariaInvalidAttribute view.root
1094+
, onFileChange
1095+
, Events.onFocus (view.onFocus attrs.identifier view.path)
1096+
, Events.onBlur (view.onBlur attrs.identifier view.path)
1097+
]
1098+
, if List.isEmpty attrs.acceptedMimeTypes then
1099+
[]
1100+
1101+
else
1102+
[ Attributes.attribute "accept" (String.join "," attrs.acceptedMimeTypes) ]
1103+
]
1104+
)
10971105
[]
10981106
, Html.label
10991107
[ Attributes.for (inputId view.root view.path)

0 commit comments

Comments
 (0)