55 */
66package org .tailormap .api .controller ;
77
8+ import com .google .common .base .Splitter ;
89import jakarta .validation .Valid ;
910import java .io .IOException ;
1011import java .io .Serializable ;
1112import java .lang .invoke .MethodHandles ;
1213import java .nio .ByteBuffer ;
1314import java .sql .SQLException ;
1415import java .util .List ;
16+ import java .util .Locale ;
1517import java .util .Set ;
1618import java .util .UUID ;
19+ import java .util .regex .Pattern ;
20+
1721import org .geotools .api .data .Query ;
1822import org .geotools .api .data .SimpleFeatureSource ;
1923import org .geotools .api .filter .Filter ;
@@ -103,22 +107,29 @@ public ResponseEntity<Serializable> addAttachment(
103107 throw new ResponseStatusException (HttpStatus .BAD_REQUEST , "Layer does not support attachments" );
104108 }
105109
106- AttachmentAttributeType attachmentAttributeType = attachmentAttrSet .stream ()
107- .filter (attr -> (attr .getAttributeName ().equals (attachment .getAttributeName ())
108- && java .util .Arrays .stream (attr .getMimeType ().split ("," ))
109- .map (String ::trim )
110- .anyMatch (mime -> mime .equals (attachment .getMimeType ()))
111- && (attr .getMaxAttachmentSize () == null || attr .getMaxAttachmentSize () >= fileData .length )))
110+ AttachmentAttributeType attachmentAttribute = attachmentAttrSet .stream ()
111+ .filter (attr -> attr .getAttributeName ().equals (attachment .getAttributeName ()))
112112 .findFirst ()
113113 .orElseThrow (() -> new ResponseStatusException (
114114 HttpStatus .BAD_REQUEST ,
115- "Layer does not support attachments for attribute "
116- + attachment .getAttributeName ()
117- + " with mime type "
118- + attachment .getMimeType ()
119- + " and size "
120- + fileData .length ));
121- logger .debug ("Using attachment attribute {}" , attachmentAttributeType );
115+ "Layer does not support attachments for attribute " + attachment .getAttributeName ()));
116+
117+ if (attachmentAttribute .getMaxAttachmentSize () != null
118+ && attachmentAttribute .getMaxAttachmentSize () < fileData .length ) {
119+ throw new ResponseStatusException (
120+ HttpStatus .BAD_REQUEST ,
121+ "Attachment size %d exceeds maximum of %d"
122+ .formatted (fileData .length , attachmentAttribute .getMaxAttachmentSize ()));
123+ }
124+
125+ if (attachmentAttribute .getMimeType () != null ) {
126+ if (!validateMimeTypeAccept (
127+ attachmentAttribute .getMimeType (), attachment .getFileName (), attachment .getMimeType ())) {
128+ throw new ResponseStatusException (HttpStatus .BAD_REQUEST , "File type or extension not allowed" );
129+ }
130+ }
131+
132+ logger .debug ("Using attachment attribute {}" , attachmentAttribute );
122133
123134 AttachmentMetadata response ;
124135 try {
@@ -130,6 +141,40 @@ public ResponseEntity<Serializable> addAttachment(
130141 return new ResponseEntity <>(response , HttpStatus .CREATED );
131142 }
132143
144+ /**
145+ * Validate as <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/file#accept">file
146+ * input "accept" attribute</a>.
147+ *
148+ * @param acceptList comma-separated list of MIME types and file extensions to validate against
149+ * @param fileName name of the file to validate
150+ * @param mimeType MIME type of the file to validate
151+ * @return true if the file's extension or MIME type matches one of the accepted types, false otherwise
152+ */
153+ private static boolean validateMimeTypeAccept (String acceptList , String fileName , String mimeType ) {
154+ Iterable <String > allowedMimeTypes = Splitter .on (Pattern .compile (",\\ s*" )).split (acceptList );
155+ final Locale locale = Locale .ENGLISH ;
156+ for (String allowedType : allowedMimeTypes ) {
157+ if (allowedType .startsWith ("." )) {
158+ // Check file extension
159+ if (fileName .toLowerCase (locale ).endsWith (allowedType .toLowerCase (locale ))) {
160+ return true ;
161+ }
162+ } else if (allowedType .endsWith ("/*" )) {
163+ // Check mime type category (e.g. image/*)
164+ String category = allowedType .substring (0 , allowedType .length () - 1 );
165+ if (mimeType .startsWith (category )) {
166+ return true ;
167+ }
168+ } else {
169+ // Check exact mime type match
170+ if (mimeType .equals (allowedType )) {
171+ return true ;
172+ }
173+ }
174+ }
175+ return false ;
176+ }
177+
133178 /**
134179 * List attachments for a feature.
135180 *
0 commit comments