Skip to content

Commit d187c65

Browse files
authored
Enable multipart/related on FileUpload (#315)
* 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
1 parent 0b50339 commit d187c65

File tree

11 files changed

+280
-6
lines changed

11 files changed

+280
-6
lines changed

commons-fileupload2-core/src/main/java/org/apache/commons/fileupload2/core/AbstractRequestContext.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,14 @@
2020
import java.util.Objects;
2121
import java.util.function.Function;
2222
import java.util.function.LongSupplier;
23+
import java.util.regex.Pattern;
2324

2425
public abstract class AbstractRequestContext<T> implements RequestContext {
26+
/**
27+
* The Content-Type Pattern for multipart/related Requests.
28+
*/
29+
private static final Pattern MULTIPART_RELATED =
30+
Pattern.compile("^\\s*multipart/related.*", Pattern.CASE_INSENSITIVE);
2531

2632
/**
2733
* Supplies the content length default.
@@ -79,4 +85,14 @@ public String toString() {
7985
return String.format("%s [ContentLength=%s, ContentType=%s]", getClass().getSimpleName(), getContentLength(), getContentType());
8086
}
8187

88+
/**
89+
* Is the Request of type <code>multipart/related</code>?
90+
*
91+
* @return the Request is of type <code>multipart/related</code>
92+
* @since 2.0.0
93+
*/
94+
@Override
95+
public boolean isMultipartRelated() {
96+
return MULTIPART_RELATED.matcher(getContentType()).matches();
97+
}
8298
}

commons-fileupload2-core/src/main/java/org/apache/commons/fileupload2/core/FileItemInputIteratorImpl.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
* The iterator returned by {@link AbstractFileUpload#getItemIterator(RequestContext)}.
3232
*/
3333
class FileItemInputIteratorImpl implements FileItemInputIterator {
34-
3534
/**
3635
* The file uploads processing utility.
3736
*
@@ -96,6 +95,11 @@ class FileItemInputIteratorImpl implements FileItemInputIterator {
9695
*/
9796
private boolean eof;
9897

98+
/**
99+
* Is the Request of type <code>multipart/related</code>.
100+
*/
101+
private final boolean multipartRelated;
102+
99103
/**
100104
* Constructs a new instance.
101105
*
@@ -109,6 +113,7 @@ class FileItemInputIteratorImpl implements FileItemInputIterator {
109113
this.sizeMax = fileUploadBase.getSizeMax();
110114
this.fileSizeMax = fileUploadBase.getFileSizeMax();
111115
this.requestContext = Objects.requireNonNull(requestContext, "requestContext");
116+
this.multipartRelated = this.requestContext.isMultipartRelated();
112117
this.skipPreamble = true;
113118
findNextItem();
114119
}
@@ -147,7 +152,16 @@ private boolean findNextItem() throws FileUploadException, IOException {
147152
continue;
148153
}
149154
final var headers = fileUpload.getParsedHeaders(multi.readHeaders());
150-
if (currentFieldName == null) {
155+
if (multipartRelated) {
156+
currentFieldName = "";
157+
currentItem = new FileItemInputImpl(
158+
this, null, null, headers.getHeader(AbstractFileUpload.CONTENT_TYPE),
159+
false, getContentLength(headers));
160+
currentItem.setHeaders(headers);
161+
progressNotifier.noteItem();
162+
itemValid = true;
163+
return true;
164+
} else if (currentFieldName == null) {
151165
// We're parsing the outer multipart
152166
final var fieldName = fileUpload.getFieldName(headers);
153167
if (fieldName != null) {

commons-fileupload2-core/src/main/java/org/apache/commons/fileupload2/core/RequestContext.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,11 @@ default Charset getCharset() throws UnsupportedCharsetException {
7070
*/
7171
InputStream getInputStream() throws IOException;
7272

73+
/**
74+
* Is the Request of type <code>multipart/related</code>?
75+
*
76+
* @return the Request is of type <code>multipart/related</code>
77+
* @since 2.0.0
78+
*/
79+
boolean isMultipartRelated();
7380
}

commons-fileupload2-core/src/test/java/org/apache/commons/fileupload2/core/AbstractFileUploadTest.java

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,4 +393,53 @@ public void testIE5MacBug() throws FileUploadException {
393393
assertTrue(field2.isFormField());
394394
assertEquals("fieldValue2", field2.getString());
395395
}
396+
397+
/**
398+
* Test for multipart/related without any content-disposition Header.
399+
* This kind of Content-Type is commonly used by SOAP-Requests with Attachments (MTOM)
400+
*/
401+
@Test
402+
public void testMultipleRelated() throws Exception {
403+
final String soapEnvelope =
404+
"<soap:Envelope xmlns:soap=\"http://www.w3.org/2003/05/soap-envelope\">\r\n" +
405+
" <soap:Header></soap:Header>\r\n" +
406+
" <soap:Body>\r\n" +
407+
" <ns1:Test xmlns:ns1=\"http://www.test.org/some-test-namespace\">\r\n" +
408+
" <ns1:Attachment>\r\n" +
409+
" <xop:Include xmlns:xop=\"http://www.w3.org/2004/08/xop/include\"" +
410+
" href=\"ref-to-attachment%40some.domain.org\"/>\r\n" +
411+
" </ns1:Attachment>\r\n" +
412+
" </ns1:Test>\r\n" +
413+
" </soap:Body>\r\n" +
414+
"</soap:Envelope>";
415+
416+
final String text =
417+
"-----1234\r\n" +
418+
"content-type: application/xop+xml; type=\"application/soap+xml\"\r\n" +
419+
"\r\n" +
420+
soapEnvelope + "\r\n" +
421+
"-----1234\r\n" +
422+
"Content-type: text/plain\r\n" +
423+
"content-id: <ref-to-attachment@some.domain.org>\r\n" +
424+
"\r\n" +
425+
"some text/plain content\r\n" +
426+
"-----1234--\r\n";
427+
428+
final var bytes = text.getBytes(StandardCharsets.US_ASCII);
429+
final var fileItems = parseUpload(upload, bytes, "multipart/related; boundary=---1234;" +
430+
" type=\"application/xop+xml\"; start-info=\"application/soap+xml\"");
431+
assertEquals(2, fileItems.size());
432+
433+
final var part1 = fileItems.get(0);
434+
assertNull(part1.getFieldName());
435+
assertFalse(part1.isFormField());
436+
assertEquals(soapEnvelope, part1.getString());
437+
438+
final var part2 = fileItems.get(1);
439+
assertNull(part2.getFieldName());
440+
assertFalse(part2.isFormField());
441+
assertEquals("some text/plain content", part2.getString());
442+
assertEquals("text/plain", part2.getContentType());
443+
assertNull(part2.getName());
444+
}
396445
}
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.commons.fileupload2.core;
18+
19+
import org.junit.jupiter.api.Test;
20+
21+
import java.io.InputStream;
22+
import java.nio.charset.StandardCharsets;
23+
import java.nio.charset.UnsupportedCharsetException;
24+
import java.util.function.Function;
25+
import java.util.function.LongSupplier;
26+
27+
import static org.junit.jupiter.api.Assertions.*;
28+
29+
/**
30+
* Tests for {@link AbstractRequestContext}
31+
*/
32+
public class MockRequestContextTest {
33+
/**
34+
* Test if the <code>content-length</code> Value is numeric.
35+
*/
36+
@Test
37+
public void getContentLengthByParsing() {
38+
final RequestContext request = new MockRequestContext(
39+
x -> "1234",
40+
() -> 5678L,
41+
"Request",
42+
"US-ASCII",
43+
"text/plain",
44+
null);
45+
assertEquals(1234L, request.getContentLength());
46+
}
47+
48+
/**
49+
* Test if the <code>content-length</code> Value is not numeric
50+
* and the Default will be taken.
51+
*/
52+
@Test
53+
public void getContentLengthDefaultBecauseOfInvalidNumber() {
54+
final RequestContext request = new MockRequestContext(
55+
x -> "not-a-number",
56+
() -> 5678L,
57+
"Request",
58+
"US-ASCII",
59+
"text/plain",
60+
null);
61+
assertEquals(5678L, request.getContentLength());
62+
}
63+
64+
/**
65+
* Test if the given <code>character-encoding</code> is a valid CharEncoding
66+
*/
67+
@Test
68+
public void getCharset() {
69+
final RequestContext request = new MockRequestContext(
70+
x -> "1234",
71+
() -> 5678L,
72+
"Request",
73+
"US-ASCII",
74+
"text/plain",
75+
null);
76+
assertEquals(StandardCharsets.US_ASCII, request.getCharset());
77+
}
78+
79+
/**
80+
* Test if the given <code>character-encoding</code> is an invalid CharEncoding
81+
* and leads to {@link UnsupportedCharsetException}
82+
*/
83+
@Test
84+
public void getInvalidCharset() {
85+
final RequestContext request = new MockRequestContext(
86+
x -> "1234",
87+
() -> 5678L,
88+
"Request",
89+
"invalid-charset",
90+
"text/plain",
91+
null);
92+
assertThrows(UnsupportedCharsetException.class, request::getCharset);
93+
}
94+
95+
/**
96+
* Test the <code>toString()</code> Output
97+
*/
98+
@Test
99+
public void testToString() {
100+
final RequestContext request = new MockRequestContext(
101+
x -> "1234",
102+
() -> 5678L,
103+
"Request",
104+
"US-ASCII",
105+
"text/plain",
106+
null);
107+
assertEquals("MockRequestContext [ContentLength=1234, ContentType=text/plain]", request.toString());
108+
}
109+
110+
/**
111+
* Test if the <code>content-type</code> is <code>multipart/related</code>
112+
*/
113+
@Test
114+
public void testIsMultipartRelated() {
115+
final RequestContext request = new MockRequestContext(
116+
x -> "1234",
117+
() -> 5678L,
118+
"Request",
119+
"US-ASCII",
120+
"multipart/related; boundary=---1234; type=\"application/xop+xml\"; start-info=\"application/soap+xml\"",
121+
null);
122+
assertTrue(request.isMultipartRelated());
123+
}
124+
125+
/**
126+
* Test if the <code>content-type</code> is not <code>multipart/related</code>
127+
*/
128+
@Test
129+
public void testIsNotMultipartRelated() {
130+
final RequestContext request = new MockRequestContext(
131+
x -> "1234",
132+
() -> 5678L,
133+
"Request",
134+
"US-ASCII",
135+
"text/plain",
136+
null);
137+
assertFalse(request.isMultipartRelated());
138+
}
139+
140+
private static final class MockRequestContext extends AbstractRequestContext<Object> {
141+
private final String characterEncoding;
142+
private final String contentType;
143+
private final InputStream inputStream;
144+
145+
private MockRequestContext(Function<String, String> contentLengthString,
146+
LongSupplier contentLengthDefault,
147+
Object request,
148+
String characterEncoding,
149+
String contentType,
150+
InputStream inputStream) {
151+
super(contentLengthString, contentLengthDefault, request);
152+
this.characterEncoding = characterEncoding;
153+
this.contentType = contentType;
154+
this.inputStream = inputStream;
155+
}
156+
157+
/**
158+
* Gets the character encoding for the request.
159+
*
160+
* @return The character encoding for the request.
161+
*/
162+
@Override
163+
public String getCharacterEncoding() {
164+
return characterEncoding;
165+
}
166+
167+
/**
168+
* Gets the content type of the request.
169+
*
170+
* @return The content type of the request.
171+
*/
172+
@Override
173+
public String getContentType() {
174+
return contentType;
175+
}
176+
177+
/**
178+
* Gets the input stream for the request.
179+
*
180+
* @return The input stream for the request.
181+
*/
182+
@Override
183+
public InputStream getInputStream() {
184+
return inputStream;
185+
}
186+
}
187+
}

commons-fileupload2-jakarta-servlet5/src/main/java/org/apache/commons/fileupload2/jakarta/servlet5/JakartaServletRequestContext.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,5 +67,4 @@ public String getContentType() {
6767
public InputStream getInputStream() throws IOException {
6868
return getRequest().getInputStream();
6969
}
70-
7170
}

commons-fileupload2-jakarta-servlet6/src/main/java/org/apache/commons/fileupload2/jakarta/servlet6/JakartaServletRequestContext.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,5 +67,4 @@ public String getContentType() {
6767
public InputStream getInputStream() throws IOException {
6868
return getRequest().getInputStream();
6969
}
70-
7170
}

commons-fileupload2-javax/src/main/java/org/apache/commons/fileupload2/javax/JavaxServletRequestContext.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,5 +67,4 @@ public String getContentType() {
6767
public InputStream getInputStream() throws IOException {
6868
return getRequest().getInputStream();
6969
}
70-
7170
}

commons-fileupload2-portlet/src/main/java/org/apache/commons/fileupload2/portlet/JavaxPortletRequestContext.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,5 +67,4 @@ public String getContentType() {
6767
public InputStream getInputStream() throws IOException {
6868
return getRequest().getPortletInputStream();
6969
}
70-
7170
}

pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,10 @@
201201
<name>Martin Grigorov</name>
202202
<email>mgrigorov@apache.org</email>
203203
</contributor>
204+
<contributor>
205+
<name>mufasa1976</name>
206+
<email>mufasa1976@coolstuff.software</email>
207+
</contributor>
204208
</contributors>
205209

206210
<scm>

0 commit comments

Comments
 (0)