This is an automated email from the ASF dual-hosted git repository. papegaaij pushed a commit to branch wicket-8.x in repository https://gitbox.apache.org/repos/asf/wicket.git
commit 9bc8cbd6e59c5ab0193c477e11e567d57316159e Author: Emond Papegaaij <[email protected]> AuthorDate: Mon Aug 3 17:20:15 2026 +0200 Enforce the configured upload limits on the Servlet Part fallback MultipartServletWebRequestImpl#parseFileParts() parses with commons-fileupload, which newFileUpload() configures from the form's maxSize, fileMaxSize and fileCountMax. When that parse yields no items it falls back to readServlet3Parts(), which wrapped whatever HttpServletRequest#getParts() returned in a ServletPartFileItem and applied none of those limits. Nothing downstream re-checks - Form#handleMultiPart() only reacts to a FileUploadException raised during parsing - so a quiet return left the limits inert. That fallback is not an error path. It is the normal one wherever something else has already consumed the request body: a @MultipartConfig servlet, Spring Boot's multipart resolver, or any filter calling getParameter() on a multipart request, which is why it was added for WICKET-5924. It is also the only path while uploadProgressUpdatesEnabled is left at its default of false. The two per-item limits, fileMaxSize and fileCountMax, are the ones this affected. Both are consulted per parsed item, and the fallback is taken precisely when there are no items, so neither was ever applied to the parts the container had already produced. An upload could therefore exceed the configured size or count, up to whatever the component that parsed the request allowed. The aggregate maxSize was applied on this path already. FileItemInputIteratorImpl#init() compares Content-Length against sizeMax before reading the body, and that header survives the body having been consumed, so a request declaring an oversized length is rejected on the commons-fileupload path and never reaches the fallback; confirmed by reading the library. declaredContentLengthOverMaxSizeIsRejectedBeforeTheFallback pins it down, because the other maxSize tests here would otherwise suggest the limit was not applied at all: they only exercise the fallback because MockHttpServletRequest reports the length of the near-empty body it builds for a parts-only request. readServlet3Parts() now applies all three limits as it builds the item list, mirroring commons-fileupload's own semantics so that both paths accept and reject the same requests: fileMaxSize against every part, form fields included, as FileItemInputImpl has no form-field guard; fileCountMax rejecting at count == max, with a negative value meaning unlimited; and maxSize against a running total of the part sizes. The aggregate check is for completeness rather than a fix for anything observed - it can only matter for a request that declares no length at all, which is conceivable through chunked transfer encoding but has not been demonstrated to reach the fallback with parts attached. The checks read only Part#getSize(), so they complete before any part is opened. That ordering is deliberate rather than incidental: parseFileParts() calls getString() on every form field, which pulls the whole part into a single heap array through IOUtils#toByteArray, so a check running after the parts had been collected would allocate exactly what it was meant to refuse - for any part arriving without a Content-Type, which is what ServletPartFileItem treats as a form field. The test asserts the part's stream is never opened, so a later reordering fails the build rather than quietly reintroducing it. The exceptions are the types the upload entry points dispatch on, so the existing feedback messages are unchanged: FileUploadByteCountLimitException for a single file, FileUploadFileCountLimitException for the count, and FileUploadSizeException for the total. Both Form#onFileUploadException() and AbstractFileUploadResource#getFileUploadExceptionKey() map those three to the UPLOAD_* resource keys; AjaxFileDropBehavior routes any FileUploadException to onError(). Note the constructors do not agree on argument order - byte count takes (actual, permitted), size takes (permitted, actual). All three entry points that accept uploads this way - Form#handleMultiPart(), AbstractFileUploadResource behind FileUploadToResourceField, and AjaxFileDropBehavior#onEvent() - set the limits and then call parseFileParts(), so enforcing in readServlet3Parts() covers each of them in one place. Also removes the per-file loop in newMultipartWebRequest(). It could not fire on the Form path: Form#handleMultiPart() reaches ServletWebRequest#newMultipartWebRequest(), which constructs a fresh instance, and even when the override was reached, files was empty and fileMaxSize unset, both being set afterwards. Its FIXME asked why the sum of the file sizes is never compared against maxSize; it is, by commons-fileupload, against the whole request stream. The tests assert the corrected behaviour. Five of the eleven fail without this change. The other six - the commons-fileupload control, the Content-Length behaviour above, delivery of files and of form fields within the limits, the count boundary, and the unconfigured defaults of Bytes.MAX / null / -1 - hold either way, and guard against the enforcement rejecting uploads it should not. Reported and originally fixed by GitHub: @deprrous. Co-Authored-By: Claude Opus 5 <[email protected]> --- .../servlet/MultipartServletWebRequestImpl.java | 76 +++- .../upload/FileUploadServletPartLimitsTest.java | 484 +++++++++++++++++++++ 2 files changed, 539 insertions(+), 21 deletions(-) diff --git a/wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/MultipartServletWebRequestImpl.java b/wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/MultipartServletWebRequestImpl.java index 355f8442a9..857e4fa537 100644 --- a/wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/MultipartServletWebRequestImpl.java +++ b/wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/MultipartServletWebRequestImpl.java @@ -29,6 +29,7 @@ import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.Part; +import org.apache.commons.fileupload.FileCountLimitExceededException; import org.apache.commons.fileupload.FileItem; import org.apache.commons.fileupload.FileItemFactory; import org.apache.commons.fileupload.FileUploadBase; @@ -247,11 +248,25 @@ public class MultipartServletWebRequestImpl extends MultipartServletWebRequest * * <strong>Note</strong>: By using Servlet 3.0 APIs the application won't be able to use * upload progress updates. + * <p> + * The container has already parsed these parts, so the limits {@link #newFileUpload(String)} + * hands to commons-fileupload never had a chance to apply to them. They are applied here instead, + * with the same semantics and the same exception types, so that a request arriving along this path + * is accepted or rejected exactly as it would have been had commons-fileupload done the parsing. + * The checks use {@link Part#getSize()} and so complete before any part is read, which matters + * because {@link #parseFileParts()} materialises every form field in memory. + * <p> + * The per-file and file-count limits are the ones this path did not apply before. The aggregate + * {@code maxSize} is also checked, though commons-fileupload has normally rejected an oversized + * request already by comparing {@code Content-Length} against it before reading the body; the + * check below covers a request that declares no length at all. * * @param request * The http request with the upload data * @return A list of {@link FileItem}s * @throws FileUploadException + * if the parts exceed the maximum upload size, the maximum size of a single file, or + * the maximum number of files */ private List<FileItem> readServlet3Parts(HttpServletRequest request) throws FileUploadException { @@ -261,9 +276,45 @@ public class MultipartServletWebRequestImpl extends MultipartServletWebRequest Collection<Part> parts = request.getParts(); if (parts != null) { + final long fileCountMax = getFileCountMax(); + final Bytes fileMaxSize = getFileMaxSize(); + final long maxSize = getMaxSize().bytes(); + long totalSize = 0; + for (Part part : parts) { + // a negative fileCountMax means unlimited, as in FileUploadBase + if (fileCountMax >= 0 && itemsFromParts.size() >= fileCountMax) + { + throw new FileCountLimitExceededException( + String.format("Request '%s' failed: Maximum file count %,d exceeded.", + FileUploadBase.MULTIPART_FORM_DATA, fileCountMax), + fileCountMax); + } + FileItem fileItem = new ServletPartFileItem(part); + long size = fileItem.getSize(); + + // commons-fileupload applies this to every part, form fields included + if (fileMaxSize != null && size > fileMaxSize.bytes()) + { + FileUploadBase.FileSizeLimitExceededException fslex = new FileUploadBase.FileSizeLimitExceededException( + String.format("The field %s exceeds its maximum permitted size of %s bytes.", + fileItem.getFieldName(), fileMaxSize.bytes()), + size, fileMaxSize.bytes()); + fslex.setFileName(fileItem.getName()); + fslex.setFieldName(fileItem.getFieldName()); + throw fslex; + } + + totalSize += size; + if (totalSize > maxSize) + { + throw new FileUploadBase.SizeLimitExceededException(String.format( + "the request was rejected because its size (%s) exceeds the configured maximum (%s)", + totalSize, maxSize), totalSize, maxSize); + } + itemsFromParts.add(fileItem); } } @@ -489,27 +540,10 @@ public class MultipartServletWebRequestImpl extends MultipartServletWebRequest public MultipartServletWebRequest newMultipartWebRequest(Bytes maxSize, String upload) throws FileUploadException { - // FIXME mgrigorov: Why these checks are made here ?! - // Why they are not done also at org.apache.wicket.protocol.http.servlet.MultipartServletWebRequestImpl.newMultipartWebRequest(org.apache.wicket.util.lang.Bytes, java.lang.String, org.apache.wicket.util.upload.FileItemFactory)() ? - // Why there is no check that the summary of all files' sizes is less than the set maxSize ? - // Setting a breakpoint here never breaks with the standard upload examples. - - Bytes fileMaxSize = getFileMaxSize(); - for (Map.Entry<String, List<FileItem>> entry : files.entrySet()) - { - List<FileItem> fileItems = entry.getValue(); - for (FileItem fileItem : fileItems) - { - if (fileMaxSize != null && fileItem.getSize() > fileMaxSize.bytes()) - { - String fieldName = entry.getKey(); - FileUploadException fslex = new FileUploadBase.FileSizeLimitExceededException("The field '" + - fieldName + "' exceeds its maximum permitted size of '" + - maxSize + "' characters.", fileItem.getSize(), fileMaxSize.bytes()); - throw fslex; - } - } - } + // This request is already multipart, so there is nothing to create. The limits are enforced by + // parseFileParts(), on both the commons-fileupload and the Servlet parts path, which is after + // this method runs: Form#handleMultiPart() sets fileMaxSize and fileCountMax and only then + // parses. return this; } diff --git a/wicket-core/src/test/java/org/apache/wicket/markup/html/form/upload/FileUploadServletPartLimitsTest.java b/wicket-core/src/test/java/org/apache/wicket/markup/html/form/upload/FileUploadServletPartLimitsTest.java new file mode 100644 index 0000000000..56116d139d --- /dev/null +++ b/wicket-core/src/test/java/org/apache/wicket/markup/html/form/upload/FileUploadServletPartLimitsTest.java @@ -0,0 +1,484 @@ +/* + * 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.wicket.markup.html.form.upload; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.Part; + +import org.apache.commons.fileupload.FileUploadBase; +import org.apache.commons.fileupload.FileUploadException; +import org.apache.wicket.markup.html.form.Form; +import org.apache.wicket.protocol.http.mock.MockHttpServletRequest; +import org.apache.wicket.protocol.http.servlet.MultipartServletWebRequestImpl; +import org.apache.wicket.request.resource.AbstractResource; +import org.apache.wicket.util.lang.Bytes; +import org.apache.wicket.util.tester.WicketTestCase; +import org.junit.Test; + +/** + * Asserts that the upload limits a {@link Form} configures are enforced when + * {@code MultipartServletWebRequestImpl#parseFileParts()} falls back to the Servlet multipart API. + * <p> + * {@code parseFileParts()} first parses with commons-fileupload, which {@code newFileUpload()} + * configures from those limits. If that yields no items it falls back to + * {@code readServlet3Parts()}, which wraps whatever {@code HttpServletRequest#getParts()} returns in + * a {@code ServletPartFileItem}. The container has already parsed those parts, so the limits handed + * to commons-fileupload never had a chance to apply to them and {@code readServlet3Parts()} has to + * apply {@code maxSize}, {@code fileMaxSize} and {@code fileCountMax} itself. Nothing downstream + * re-checks: {@code Form#handleMultiPart()} only reacts to a {@link FileUploadException} raised + * during parsing, so a quiet return on this path means the limits are simply gone. + * <p> + * The fallback is not an error path. It exists for deployments where something else has already + * consumed the request body - a {@code @MultipartConfig} servlet, Spring Boot's multipart resolver, + * or any filter touching {@code request.getParameter()} on a multipart request - which is why it was + * added for <a href="https://issues.apache.org/jira/browse/WICKET-5924">WICKET-5924</a>. There it is + * the normal path, and it is the only path while {@code uploadProgressUpdatesEnabled} is left at its + * default of {@code false}. + * <p> + * Two properties of the fallback make the enforcement worth pinning down in detail. Rejection has to + * happen before a part is read, because {@code parseFileParts()} materialises every form field with + * {@code ServletPartFileItem#getString()}, which pulls the whole part into a single heap array + * through {@code IOUtils#toByteArray} - a check running after the parts had been collected would + * allocate exactly what it was meant to refuse. See + * {@link #oversizedFormFieldPartIsRejectedBeforeItIsBuffered()}. And {@code maxSize} was already + * applied to this path: commons-fileupload compares {@code Content-Length} against it before reading + * anything, so a request declaring an oversized length is rejected before the fallback is taken - see + * {@link #declaredContentLengthOverMaxSizeIsRejectedBeforeTheFallback()}. The aggregate check + * {@code readServlet3Parts()} performs is for completeness, not a fix for something observed. + * <p> + * {@code maxPartHeaderSize}, which {@code newFileUpload()} also configures, is deliberately not + * covered here: on this path the container has already parsed the part headers under its own rules, + * so Wicket's value was never going to apply to them. + * <p> + * These tests drive {@code MultipartServletWebRequestImpl} in the same order + * {@code Form#handleMultiPart()} does, rather than going through a page, so that nothing but the + * limit handling is under test. {@link #limitsAreEnforcedOnTheCommonsFileUploadPath()} is the + * control: it shows the same limit honoured when commons-fileupload does the parsing, which keeps + * the suite honest about which of the two paths each other test exercises. + */ +public class FileUploadServletPartLimitsTest extends WicketTestCase +{ + /** Comfortably over every limit configured below. */ + private static final int OVERSIZED = 10_000; + + /** + * The size of the form field part. Large enough that buffering it would be unmistakably deliberate + * rather than a rounding error, small enough to be harmless in a test. + */ + private static final int LARGE = 1 << 20; + + private static final Bytes LIMIT = Bytes.bytes(100); + + /** Comfortably inside {@link #LIMIT}, for the tests that assert an upload is let through. */ + private static final int WITHIN_LIMIT = 10; + + private static final String FIELD = "upload"; + + /** + * Registers {@code parts} on a multipart request that carries no body of its own, so that + * commons-fileupload finds nothing and the Servlet fallback is taken - the same situation as a + * container which has already parsed the upload. + */ + private MockHttpServletRequest requestWithParts(Part... parts) + { + MockHttpServletRequest request = tester.getRequest(); + request.setUseMultiPartContentType(true); + for (int i = 0; i < parts.length; i++) + { + request.setPart(FIELD + i, parts[i]); + } + return request; + } + + /** + * Parses exactly as {@code Form#handleMultiPart()} does: construct with {@code maxSize}, then set + * the per-file and count limits, then parse. + */ + private MultipartServletWebRequestImpl parse(HttpServletRequest request, Bytes maxSize, + Bytes fileMaxSize, Long fileCountMax) throws FileUploadException + { + MultipartServletWebRequestImpl multipart = + new MultipartServletWebRequestImpl(request, "", maxSize, "uploadId"); + multipart.setFileMaxSize(fileMaxSize); + if (fileCountMax != null) + { + multipart.setFileCountMax(fileCountMax); + } + multipart.parseFileParts(); + return multipart; + } + + /** + * {@link MockHttpServletRequest#getContentLength()} reports the length of the body it builds, which + * for a request carrying only pre-parsed parts is a few dozen bytes. A real request declares the + * length of the upload it carried, so that has to be substituted to see what happens on the + * commons-fileupload path. + */ + private HttpServletRequest withDeclaredContentLength(MockHttpServletRequest request, + int contentLength) + { + return new HttpServletRequestWrapper(request) + { + @Override + public int getContentLength() + { + return contentLength; + } + + @Override + public long getContentLengthLong() + { + return contentLength; + } + }; + } + + /** + * Records where {@code maxSize} is really enforced when the container has already parsed the body. + * commons-fileupload compares the declared {@code Content-Length} against {@code maxSize} before it + * reads anything, and that header survives the body having been consumed, so an oversized + * <em>declared</em> length is rejected on the commons-fileupload path and the fallback is never + * reached. This holds independently of the enforcement below - it is commons-fileupload doing the + * work - and is asserted so that the division of labour between the two is not lost. + * <p> + * So the aggregate limit was not what went unapplied here; {@code fileMaxSize} and + * {@code fileCountMax} were, being per-item checks that are consulted only when there are items, + * and the fallback is taken precisely when there are none. The other {@code maxSize} tests here + * exercise the aggregate check {@code readServlet3Parts()} performs regardless. That check can only + * matter for a request declaring no length at all - conceivable through chunked transfer encoding, + * but not demonstrated against a real container - so it is kept for completeness rather than as a + * fix for anything observed. + */ + @Test + public void declaredContentLengthOverMaxSizeIsRejectedBeforeTheFallback() + { + MockHttpServletRequest request = requestWithParts(new SizedPart(FIELD, OVERSIZED)); + + FileUploadBase.SizeLimitExceededException e = assertThrows(FileUploadBase.SizeLimitExceededException.class, + () -> parse(withDeclaredContentLength(request, OVERSIZED), LIMIT, null, null), + "a declared Content-Length over maxSize must be rejected"); + + // pins it to the request-size check rather than any other parse failure + assertEquals("permitted size should be maxSize", LIMIT.bytes(), e.getPermittedSize()); + assertEquals("actual size should be the declared length", OVERSIZED, e.getActualSize()); + } + + /** + * A part far larger than {@code fileMaxSize}. + */ + @Test + public void fileMaxSizeIsEnforcedOnServletPartFallback() + { + MockHttpServletRequest request = requestWithParts(new SizedPart(FIELD, OVERSIZED)); + + assertThrows(FileUploadException.class, + () -> parse(request, Bytes.MAX, LIMIT, null), + "a part exceeding fileMaxSize must be rejected"); + } + + /** + * A single part far larger than the total {@code maxSize}, on a request that does not declare a + * length large enough for commons-fileupload to have rejected it first. Covers the aggregate check + * kept for completeness, not something that went unapplied - see + * {@link #declaredContentLengthOverMaxSizeIsRejectedBeforeTheFallback()}. + */ + @Test + public void maxSizeIsEnforcedOnServletPartFallback() + { + MockHttpServletRequest request = requestWithParts(new SizedPart(FIELD, OVERSIZED)); + + assertThrows(FileUploadException.class, + () -> parse(request, LIMIT, null, null), + "an upload exceeding the form's maxSize must be rejected"); + } + + /** + * More parts than {@code fileCountMax} allows. + */ + @Test + public void fileCountMaxIsEnforcedOnServletPartFallback() + { + MockHttpServletRequest request = requestWithParts(new SizedPart(FIELD, 1), + new SizedPart(FIELD, 1), new SizedPart(FIELD, 1)); + + assertThrows(FileUploadException.class, + () -> parse(request, Bytes.MAX, null, 1L), + "more parts than fileCountMax must be rejected"); + } + + /** + * Parts each within {@code fileMaxSize} but together over {@code maxSize}. Neither trips the + * per-part check, so only the running total catches this. For completeness, as above. + */ + @Test + public void aggregateSizeOfPartsIsEnforcedAgainstMaxSize() + { + int half = (int)LIMIT.bytes() - 20; + MockHttpServletRequest request = + requestWithParts(new SizedPart(FIELD, half), new SizedPart(FIELD, half)); + + assertThrows(FileUploadException.class, + () -> parse(request, LIMIT, Bytes.bytes(half), null), + "parts summing to more than maxSize must be rejected"); + } + + /** + * An oversized form field part - one with no {@code Content-Type} - must be rejected without being + * read, since {@code parseFileParts()} would otherwise buffer it whole. The part fails the test if + * its stream is opened at all, which is what makes this a guard against the checks being reordered + * after the parts have been collected rather than just another rejection test. + */ + @Test + public void oversizedFormFieldPartIsRejectedBeforeItIsBuffered() + { + SizedPart part = new SizedPart(FIELD, LARGE, null); + MockHttpServletRequest request = requestWithParts(part); + + assertThrows(FileUploadException.class, + () -> parse(request, LIMIT, null, null), + "a form field part exceeding maxSize must be rejected"); + + assertFalse("the part must be rejected before its content is read", part.wasRead()); + } + + /** + * The fallback still delivers what it should. This is also the assurance that the tests above + * exercise the fallback at all rather than failing for an unrelated reason: the same construction + * that is rejected when oversized arrives as an uploaded file when it fits. + */ + @Test + public void fallbackDeliversPartsWithinTheLimits() throws Exception + { + MockHttpServletRequest request = requestWithParts(new SizedPart(FIELD, WITHIN_LIMIT)); + + MultipartServletWebRequestImpl multipart = parse(request, LIMIT, LIMIT, 1L); + + List<org.apache.commons.fileupload.FileItem> files = multipart.getFile(FIELD); + assertNotNull("the part should have come through the fallback", files); + assertEquals("exactly one part was uploaded", 1, files.size()); + assertEquals("the part should arrive intact", WITHIN_LIMIT, files.get(0).getSize()); + } + + /** + * A form field part within the limits still becomes a request parameter, so the enforcement has not + * cost the fallback its handling of form fields. + */ + @Test + public void fallbackDeliversFormFieldPartsWithinTheLimits() throws Exception + { + MockHttpServletRequest request = + requestWithParts(new SizedPart(FIELD, WITHIN_LIMIT, null)); + + MultipartServletWebRequestImpl multipart = parse(request, LIMIT, LIMIT, null); + + assertEquals("the form field should arrive intact", WITHIN_LIMIT, + multipart.getPostParameters().getParameterValue(FIELD).toString().length()); + } + + /** + * Exactly {@code fileCountMax} parts are allowed through; it is the part after the limit that is + * refused, as in {@code FileUploadBase}. + */ + @Test + public void exactlyFileCountMaxPartsAreAccepted() throws Exception + { + MockHttpServletRequest request = requestWithParts(new SizedPart(FIELD, WITHIN_LIMIT), + new SizedPart(FIELD, WITHIN_LIMIT), new SizedPart(FIELD, WITHIN_LIMIT)); + + MultipartServletWebRequestImpl multipart = parse(request, Bytes.MAX, null, 3L); + + assertEquals("all three parts are within fileCountMax", 3, multipart.getFile(FIELD).size()); + } + + /** + * An application that configures no limits keeps accepting everything: {@code maxSize} defaults to + * {@link Bytes#MAX}, {@code fileMaxSize} to {@code null} and {@code fileCountMax} to {@code -1}. + * Guards against the enforcement breaking deployments that never asked for a limit. + */ + @Test + public void nothingConfiguredAcceptsLargeAndNumerousParts() throws Exception + { + MockHttpServletRequest request = requestWithParts(new SizedPart(FIELD, OVERSIZED), + new SizedPart(FIELD, OVERSIZED), new SizedPart(FIELD, OVERSIZED)); + + MultipartServletWebRequestImpl multipart = parse(request, Bytes.MAX, null, null); + + assertEquals("no limit was configured", 3, multipart.getFile(FIELD).size()); + } + + /** + * The control: with a real multipart body, commons-fileupload does the parsing and the very same + * limit is honoured. Holds with or without the enforcement on the fallback, which is what makes it + * a control. + */ + @Test + public void limitsAreEnforcedOnTheCommonsFileUploadPath() throws Exception + { + MockHttpServletRequest request = tester.getRequest(); + request.setUseMultiPartContentType(true); + request.addFile(FIELD, oversizedFile(), "text/plain"); + + assertThrows(FileUploadException.class, + () -> parse(request, Bytes.MAX, LIMIT, null), + "commons-fileupload should still enforce fileMaxSize"); + } + + /** {@code Assertions#assertThrows} is not available on the JUnit version of this branch. */ + private static <T extends Throwable> T assertThrows(Class<T> expected, Executable executable, + String message) + { + try + { + executable.execute(); + } + catch (Throwable actual) + { + if (expected.isInstance(actual)) + { + return expected.cast(actual); + } + throw new AssertionError(message + " - expected " + expected.getName() + " but was " + + actual, actual); + } + throw new AssertionError(message + " - expected " + expected.getName() + + " but nothing was thrown"); + } + + private interface Executable + { + void execute() throws Throwable; + } + + private org.apache.wicket.util.file.File oversizedFile() throws IOException + { + java.io.File file = java.io.File.createTempFile("wicket-upload-limits", ".txt"); + file.deleteOnExit(); + Files.write(file.toPath(), new byte[OVERSIZED]); + return new org.apache.wicket.util.file.File(file); + } + + /** A {@link Part} reporting an arbitrary size, as a container-parsed upload would. */ + private static class SizedPart implements Part + { + private final LinkedHashMap<String, List<String>> headers = new LinkedHashMap<>(); + private final String fieldName; + private final String contentType; + private final byte[] data; + private boolean read; + + private SizedPart(String fieldName, int size) + { + this(fieldName, size, "text/plain"); + } + + /** + * @param contentType + * {@code null} for a part {@code ServletPartFileItem} will treat as a form field + */ + private SizedPart(String fieldName, int size, String contentType) + { + this.fieldName = fieldName; + this.contentType = contentType; + this.data = new byte[size]; + // printable ASCII, so that the decoded length is the part's length in any encoding + Arrays.fill(this.data, (byte)'a'); + headers.put(AbstractResource.CONTENT_DISPOSITION_HEADER_NAME, + Arrays.asList(contentType == null + ? "form-data;name=" + fieldName + : "attachment;filename=oversized.txt")); + } + + @Override + public InputStream getInputStream() + { + read = true; + return new ByteArrayInputStream(data); + } + + /** Whether anything asked for this part's content, rather than only its metadata. */ + boolean wasRead() + { + return read; + } + + @Override + public String getContentType() + { + return contentType; + } + + @Override + public String getName() + { + return fieldName; + } + + @Override + public String getSubmittedFileName() + { + return "oversized.txt"; + } + + @Override + public long getSize() + { + return data.length; + } + + @Override + public void write(String fileName) + { + } + + @Override + public void delete() + { + } + + @Override + public String getHeader(String name) + { + return headers.containsKey(name) ? headers.get(name).get(0) : null; + } + + @Override + public Collection<String> getHeaders(String name) + { + return headers.get(name); + } + + @Override + public Collection<String> getHeaderNames() + { + return headers.keySet(); + } + } +}
