Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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