Skip to content

Commit

Permalink
Enable multipart/related on FileUpload (#315)
Browse files Browse the repository at this point in the history
* added ability to use content-type: multipart/related

* added entry to src/changes/changes.xml; removed unnecessary paragraph at javadoc

* fixing checkstyle failures

* fixed findings of review

* added MockRequestContextTest

* added License Header
  • Loading branch information
mufasa1976 committed May 20, 2024
1 parent 0b50339 commit d187c65
Show file tree
Hide file tree
Showing 11 changed files with 280 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> 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.
Expand Down Expand Up @@ -79,4 +85,14 @@ public String toString() {
return String.format("%s [ContentLength=%s, ContentType=%s]", getClass().getSimpleName(), getContentLength(), getContentType());
}

/**
* Is the Request of type <code>multipart/related</code>?
*
* @return the Request is of type <code>multipart/related</code>
* @since 2.0.0
*/
@Override
public boolean isMultipartRelated() {
return MULTIPART_RELATED.matcher(getContentType()).matches();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
* The iterator returned by {@link AbstractFileUpload#getItemIterator(RequestContext)}.
*/
class FileItemInputIteratorImpl implements FileItemInputIterator {

/**
* The file uploads processing utility.
*
Expand Down Expand Up @@ -96,6 +95,11 @@ class FileItemInputIteratorImpl implements FileItemInputIterator {
*/
private boolean eof;

/**
* Is the Request of type <code>multipart/related</code>.
*/
private final boolean multipartRelated;

/**
* Constructs a new instance.
*
Expand All @@ -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();
}
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,11 @@ default Charset getCharset() throws UnsupportedCharsetException {
*/
InputStream getInputStream() throws IOException;

/**
* Is the Request of type <code>multipart/related</code>?
*
* @return the Request is of type <code>multipart/related</code>
* @since 2.0.0
*/
boolean isMultipartRelated();
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
"<soap:Envelope xmlns:soap=\"http://www.w3.org/2003/05/soap-envelope\">\r\n" +
" <soap:Header></soap:Header>\r\n" +
" <soap:Body>\r\n" +
" <ns1:Test xmlns:ns1=\"http://www.test.org/some-test-namespace\">\r\n" +
" <ns1:Attachment>\r\n" +
" <xop:Include xmlns:xop=\"http://www.w3.org/2004/08/xop/include\"" +
" href=\"ref-to-attachment%40some.domain.org\"/>\r\n" +
" </ns1:Attachment>\r\n" +
" </ns1:Test>\r\n" +
" </soap:Body>\r\n" +
"</soap:Envelope>";

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: <ref-to-attachment@some.domain.org>\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());
}
}
Original file line number Diff line number Diff line change
@@ -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 <code>content-length</code> 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 <code>content-length</code> 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 <code>character-encoding</code> 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 <code>character-encoding</code> 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 <code>toString()</code> 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 <code>content-type</code> is <code>multipart/related</code>
*/
@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 <code>content-type</code> is not <code>multipart/related</code>
*/
@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<Object> {
private final String characterEncoding;
private final String contentType;
private final InputStream inputStream;

private MockRequestContext(Function<String, String> 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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,4 @@ public String getContentType() {
public InputStream getInputStream() throws IOException {
return getRequest().getInputStream();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,4 @@ public String getContentType() {
public InputStream getInputStream() throws IOException {
return getRequest().getInputStream();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,4 @@ public String getContentType() {
public InputStream getInputStream() throws IOException {
return getRequest().getInputStream();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,4 @@ public String getContentType() {
public InputStream getInputStream() throws IOException {
return getRequest().getPortletInputStream();
}

}
4 changes: 4 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,10 @@
<name>Martin Grigorov</name>
<email>mgrigorov@apache.org</email>
</contributor>
<contributor>
<name>mufasa1976</name>
<email>mufasa1976@coolstuff.software</email>
</contributor>
</contributors>

<scm>
Expand Down
1 change: 1 addition & 0 deletions src/changes/changes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ The <action> type attribute can be add,update,fix,remove.
<action dev="ggregory" type="fix" due-to="Gregor Dschung">[site] Fix instantiation of DiskFileItemFactory in migration guide #273.</action>
<action issue="FILEUPLOAD-355" dev="ggregory" type="fix" due-to="Ana, Gary Gregory">[site] Update code example: Use IOUtils instead of Streams utils class.</action>
<!-- ADD -->
<action dev="mufasa1976" type="add">handle multipart/related Requests without content-disposition header</action>
<!-- UDPATE -->
<action dev="ggregory" type="update" due-to="Gary Gregory">Bump org.apache.commons:commons-parent from 66 to 69 #283, #294.</action>
<action dev="ggregory" type="update" due-to="Gary Gregory">Bump commons-io:commons-io from 2.16.0 to 2.16.1 #297.</action>
Expand Down

0 comments on commit d187c65

Please sign in to comment.