diff --git a/commons-fileupload2-core/src/main/java/org/apache/commons/fileupload2/core/AbstractRequestContext.java b/commons-fileupload2-core/src/main/java/org/apache/commons/fileupload2/core/AbstractRequestContext.java index f6e96fae0..175ef2d27 100644 --- a/commons-fileupload2-core/src/main/java/org/apache/commons/fileupload2/core/AbstractRequestContext.java +++ b/commons-fileupload2-core/src/main/java/org/apache/commons/fileupload2/core/AbstractRequestContext.java @@ -20,8 +20,14 @@ import java.util.Objects; import java.util.function.Function; import java.util.function.LongSupplier; +import java.util.regex.Pattern; public abstract class AbstractRequestContext implements RequestContext { + /** + * The Content-Type Pattern for multipart/related Requests. + */ + private static final Pattern MULTIPART_RELATED = + Pattern.compile("^\\s*multipart/related.*", Pattern.CASE_INSENSITIVE); /** * Supplies the content length default. @@ -79,4 +85,14 @@ public String toString() { return String.format("%s [ContentLength=%s, ContentType=%s]", getClass().getSimpleName(), getContentLength(), getContentType()); } + /** + * Is the Request of type multipart/related? + * + * @return the Request is of type multipart/related + * @since 2.0.0 + */ + @Override + public boolean isMultipartRelated() { + return MULTIPART_RELATED.matcher(getContentType()).matches(); + } } diff --git a/commons-fileupload2-core/src/main/java/org/apache/commons/fileupload2/core/FileItemInputIteratorImpl.java b/commons-fileupload2-core/src/main/java/org/apache/commons/fileupload2/core/FileItemInputIteratorImpl.java index c01f2ae37..ba8041d76 100644 --- a/commons-fileupload2-core/src/main/java/org/apache/commons/fileupload2/core/FileItemInputIteratorImpl.java +++ b/commons-fileupload2-core/src/main/java/org/apache/commons/fileupload2/core/FileItemInputIteratorImpl.java @@ -31,7 +31,6 @@ * The iterator returned by {@link AbstractFileUpload#getItemIterator(RequestContext)}. */ class FileItemInputIteratorImpl implements FileItemInputIterator { - /** * The file uploads processing utility. * @@ -96,6 +95,11 @@ class FileItemInputIteratorImpl implements FileItemInputIterator { */ private boolean eof; + /** + * Is the Request of type multipart/related. + */ + private final boolean multipartRelated; + /** * Constructs a new instance. * @@ -109,6 +113,7 @@ class FileItemInputIteratorImpl implements FileItemInputIterator { this.sizeMax = fileUploadBase.getSizeMax(); this.fileSizeMax = fileUploadBase.getFileSizeMax(); this.requestContext = Objects.requireNonNull(requestContext, "requestContext"); + this.multipartRelated = this.requestContext.isMultipartRelated(); this.skipPreamble = true; findNextItem(); } @@ -147,7 +152,16 @@ private boolean findNextItem() throws FileUploadException, IOException { continue; } final var headers = fileUpload.getParsedHeaders(multi.readHeaders()); - if (currentFieldName == null) { + if (multipartRelated) { + currentFieldName = ""; + currentItem = new FileItemInputImpl( + this, null, null, headers.getHeader(AbstractFileUpload.CONTENT_TYPE), + false, getContentLength(headers)); + currentItem.setHeaders(headers); + progressNotifier.noteItem(); + itemValid = true; + return true; + } else if (currentFieldName == null) { // We're parsing the outer multipart final var fieldName = fileUpload.getFieldName(headers); if (fieldName != null) { diff --git a/commons-fileupload2-core/src/main/java/org/apache/commons/fileupload2/core/RequestContext.java b/commons-fileupload2-core/src/main/java/org/apache/commons/fileupload2/core/RequestContext.java index 1edb7b951..1684e8174 100644 --- a/commons-fileupload2-core/src/main/java/org/apache/commons/fileupload2/core/RequestContext.java +++ b/commons-fileupload2-core/src/main/java/org/apache/commons/fileupload2/core/RequestContext.java @@ -70,4 +70,11 @@ default Charset getCharset() throws UnsupportedCharsetException { */ InputStream getInputStream() throws IOException; + /** + * Is the Request of type multipart/related? + * + * @return the Request is of type multipart/related + * @since 2.0.0 + */ + boolean isMultipartRelated(); } diff --git a/commons-fileupload2-core/src/test/java/org/apache/commons/fileupload2/core/AbstractFileUploadTest.java b/commons-fileupload2-core/src/test/java/org/apache/commons/fileupload2/core/AbstractFileUploadTest.java index ef80e45df..a41ad7e40 100644 --- a/commons-fileupload2-core/src/test/java/org/apache/commons/fileupload2/core/AbstractFileUploadTest.java +++ b/commons-fileupload2-core/src/test/java/org/apache/commons/fileupload2/core/AbstractFileUploadTest.java @@ -393,4 +393,53 @@ public void testIE5MacBug() throws FileUploadException { assertTrue(field2.isFormField()); assertEquals("fieldValue2", field2.getString()); } + + /** + * Test for multipart/related without any content-disposition Header. + * This kind of Content-Type is commonly used by SOAP-Requests with Attachments (MTOM) + */ + @Test + public void testMultipleRelated() throws Exception { + final String soapEnvelope = + "\r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + " \r\n" + + ""; + + final String text = + "-----1234\r\n" + + "content-type: application/xop+xml; type=\"application/soap+xml\"\r\n" + + "\r\n" + + soapEnvelope + "\r\n" + + "-----1234\r\n" + + "Content-type: text/plain\r\n" + + "content-id: \r\n" + + "\r\n" + + "some text/plain content\r\n" + + "-----1234--\r\n"; + + final var bytes = text.getBytes(StandardCharsets.US_ASCII); + final var fileItems = parseUpload(upload, bytes, "multipart/related; boundary=---1234;" + + " type=\"application/xop+xml\"; start-info=\"application/soap+xml\""); + assertEquals(2, fileItems.size()); + + final var part1 = fileItems.get(0); + assertNull(part1.getFieldName()); + assertFalse(part1.isFormField()); + assertEquals(soapEnvelope, part1.getString()); + + final var part2 = fileItems.get(1); + assertNull(part2.getFieldName()); + assertFalse(part2.isFormField()); + assertEquals("some text/plain content", part2.getString()); + assertEquals("text/plain", part2.getContentType()); + assertNull(part2.getName()); + } } diff --git a/commons-fileupload2-core/src/test/java/org/apache/commons/fileupload2/core/MockRequestContextTest.java b/commons-fileupload2-core/src/test/java/org/apache/commons/fileupload2/core/MockRequestContextTest.java new file mode 100644 index 000000000..e8a34f82d --- /dev/null +++ b/commons-fileupload2-core/src/test/java/org/apache/commons/fileupload2/core/MockRequestContextTest.java @@ -0,0 +1,187 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.fileupload2.core; + +import org.junit.jupiter.api.Test; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.charset.UnsupportedCharsetException; +import java.util.function.Function; +import java.util.function.LongSupplier; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests for {@link AbstractRequestContext} + */ +public class MockRequestContextTest { + /** + * Test if the content-length Value is numeric. + */ + @Test + public void getContentLengthByParsing() { + final RequestContext request = new MockRequestContext( + x -> "1234", + () -> 5678L, + "Request", + "US-ASCII", + "text/plain", + null); + assertEquals(1234L, request.getContentLength()); + } + + /** + * Test if the content-length Value is not numeric + * and the Default will be taken. + */ + @Test + public void getContentLengthDefaultBecauseOfInvalidNumber() { + final RequestContext request = new MockRequestContext( + x -> "not-a-number", + () -> 5678L, + "Request", + "US-ASCII", + "text/plain", + null); + assertEquals(5678L, request.getContentLength()); + } + + /** + * Test if the given character-encoding is a valid CharEncoding + */ + @Test + public void getCharset() { + final RequestContext request = new MockRequestContext( + x -> "1234", + () -> 5678L, + "Request", + "US-ASCII", + "text/plain", + null); + assertEquals(StandardCharsets.US_ASCII, request.getCharset()); + } + + /** + * Test if the given character-encoding is an invalid CharEncoding + * and leads to {@link UnsupportedCharsetException} + */ + @Test + public void getInvalidCharset() { + final RequestContext request = new MockRequestContext( + x -> "1234", + () -> 5678L, + "Request", + "invalid-charset", + "text/plain", + null); + assertThrows(UnsupportedCharsetException.class, request::getCharset); + } + + /** + * Test the toString() Output + */ + @Test + public void testToString() { + final RequestContext request = new MockRequestContext( + x -> "1234", + () -> 5678L, + "Request", + "US-ASCII", + "text/plain", + null); + assertEquals("MockRequestContext [ContentLength=1234, ContentType=text/plain]", request.toString()); + } + + /** + * Test if the content-type is multipart/related + */ + @Test + public void testIsMultipartRelated() { + final RequestContext request = new MockRequestContext( + x -> "1234", + () -> 5678L, + "Request", + "US-ASCII", + "multipart/related; boundary=---1234; type=\"application/xop+xml\"; start-info=\"application/soap+xml\"", + null); + assertTrue(request.isMultipartRelated()); + } + + /** + * Test if the content-type is not multipart/related + */ + @Test + public void testIsNotMultipartRelated() { + final RequestContext request = new MockRequestContext( + x -> "1234", + () -> 5678L, + "Request", + "US-ASCII", + "text/plain", + null); + assertFalse(request.isMultipartRelated()); + } + + private static final class MockRequestContext extends AbstractRequestContext { + private final String characterEncoding; + private final String contentType; + private final InputStream inputStream; + + private MockRequestContext(Function contentLengthString, + LongSupplier contentLengthDefault, + Object request, + String characterEncoding, + String contentType, + InputStream inputStream) { + super(contentLengthString, contentLengthDefault, request); + this.characterEncoding = characterEncoding; + this.contentType = contentType; + this.inputStream = inputStream; + } + + /** + * Gets the character encoding for the request. + * + * @return The character encoding for the request. + */ + @Override + public String getCharacterEncoding() { + return characterEncoding; + } + + /** + * Gets the content type of the request. + * + * @return The content type of the request. + */ + @Override + public String getContentType() { + return contentType; + } + + /** + * Gets the input stream for the request. + * + * @return The input stream for the request. + */ + @Override + public InputStream getInputStream() { + return inputStream; + } + } +} diff --git a/commons-fileupload2-jakarta-servlet5/src/main/java/org/apache/commons/fileupload2/jakarta/servlet5/JakartaServletRequestContext.java b/commons-fileupload2-jakarta-servlet5/src/main/java/org/apache/commons/fileupload2/jakarta/servlet5/JakartaServletRequestContext.java index 5b93c9066..086aedbe0 100644 --- a/commons-fileupload2-jakarta-servlet5/src/main/java/org/apache/commons/fileupload2/jakarta/servlet5/JakartaServletRequestContext.java +++ b/commons-fileupload2-jakarta-servlet5/src/main/java/org/apache/commons/fileupload2/jakarta/servlet5/JakartaServletRequestContext.java @@ -67,5 +67,4 @@ public String getContentType() { public InputStream getInputStream() throws IOException { return getRequest().getInputStream(); } - } diff --git a/commons-fileupload2-jakarta-servlet6/src/main/java/org/apache/commons/fileupload2/jakarta/servlet6/JakartaServletRequestContext.java b/commons-fileupload2-jakarta-servlet6/src/main/java/org/apache/commons/fileupload2/jakarta/servlet6/JakartaServletRequestContext.java index 2763700a2..298414128 100644 --- a/commons-fileupload2-jakarta-servlet6/src/main/java/org/apache/commons/fileupload2/jakarta/servlet6/JakartaServletRequestContext.java +++ b/commons-fileupload2-jakarta-servlet6/src/main/java/org/apache/commons/fileupload2/jakarta/servlet6/JakartaServletRequestContext.java @@ -67,5 +67,4 @@ public String getContentType() { public InputStream getInputStream() throws IOException { return getRequest().getInputStream(); } - } diff --git a/commons-fileupload2-javax/src/main/java/org/apache/commons/fileupload2/javax/JavaxServletRequestContext.java b/commons-fileupload2-javax/src/main/java/org/apache/commons/fileupload2/javax/JavaxServletRequestContext.java index e933b70cd..9cc2af3bf 100644 --- a/commons-fileupload2-javax/src/main/java/org/apache/commons/fileupload2/javax/JavaxServletRequestContext.java +++ b/commons-fileupload2-javax/src/main/java/org/apache/commons/fileupload2/javax/JavaxServletRequestContext.java @@ -67,5 +67,4 @@ public String getContentType() { public InputStream getInputStream() throws IOException { return getRequest().getInputStream(); } - } diff --git a/commons-fileupload2-portlet/src/main/java/org/apache/commons/fileupload2/portlet/JavaxPortletRequestContext.java b/commons-fileupload2-portlet/src/main/java/org/apache/commons/fileupload2/portlet/JavaxPortletRequestContext.java index 460a55e65..671b488c4 100644 --- a/commons-fileupload2-portlet/src/main/java/org/apache/commons/fileupload2/portlet/JavaxPortletRequestContext.java +++ b/commons-fileupload2-portlet/src/main/java/org/apache/commons/fileupload2/portlet/JavaxPortletRequestContext.java @@ -67,5 +67,4 @@ public String getContentType() { public InputStream getInputStream() throws IOException { return getRequest().getPortletInputStream(); } - } diff --git a/pom.xml b/pom.xml index fcbcea968..df9207830 100644 --- a/pom.xml +++ b/pom.xml @@ -201,6 +201,10 @@ Martin Grigorov mgrigorov@apache.org + + mufasa1976 + mufasa1976@coolstuff.software + diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 0549af2f4..b7f551b75 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -48,6 +48,7 @@ The type attribute can be add,update,fix,remove. [site] Fix instantiation of DiskFileItemFactory in migration guide #273. [site] Update code example: Use IOUtils instead of Streams utils class. + handle multipart/related Requests without content-disposition header Bump org.apache.commons:commons-parent from 66 to 69 #283, #294. Bump commons-io:commons-io from 2.16.0 to 2.16.1 #297.