From 5c3ca182cd0ef5423cbd05564cb53b511327fcd4 Mon Sep 17 00:00:00 2001 From: reiern70 Date: Sun, 18 May 2025 09:25:10 -0500 Subject: [PATCH] [WICKET-7154] provide a way to hook into tomcat native multipart processing and at the same time do upload progress reporting. This is needed because with tomcat 11.x tomcat will parse multipart whenever getParameters is called and logic using fileupload2 is very error-prone (it can be rather easily broken on applications if "someone" calls getParameters before wicket form processing takes place). --- pom.xml | 50 ++- wicket-core/pom.xml | 8 + wicket-core/src/main/java/module-info.java | 4 +- .../apache/wicket/markup/html/form/Form.java | 43 ++- .../html/form/upload/FileUploadField.java | 4 + .../resource/FileUploadToResourceField.java | 44 ++- .../resource/FileUploadToResourceField.js | 3 +- .../http/BufferedHttpServletResponse.java | 14 +- .../wicket/protocol/http/WebApplication.java | 22 ++ .../http/mock/MockHttpServletResponse.java | 11 + .../MultipartServletWebRequestImpl.java | 21 +- .../http/servlet/ServletPartFileItem.java | 5 +- ...tNativeMultipartServletWebRequestImpl.java | 359 ++++++++++++++++++ .../TomcatUploadProgressListenerFactory.java | 156 ++++++++ .../protocol/http/servlet/UploadInfo.java | 16 +- .../wicket/settings/ApplicationSettings.java | 25 ++ .../extensions/ajax/AjaxFileDropBehavior.java | 3 +- .../html/form/upload/UploadProgressBar.java | 9 +- .../migration/MigrateToWicket10Test.java | 2 + 19 files changed, 767 insertions(+), 32 deletions(-) create mode 100644 wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/TomcatNativeMultipartServletWebRequestImpl.java create mode 100644 wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/TomcatUploadProgressListenerFactory.java diff --git a/pom.xml b/pom.xml index 9d5db180215..6347656bc97 100644 --- a/pom.xml +++ b/pom.xml @@ -137,8 +137,8 @@ true true - 17 - 17 + 24 + 24 9.8 @@ -151,6 +151,7 @@ 2.0.0-M2 2.19.0 3.17.0 + 12.0.0-M1-SNAPSHOT 7.0.0 2.2.1-b05 2.38.0 @@ -338,6 +339,16 @@ commons-lang3 ${commons-lang3.version} + + org.apache.tomcat + tomcat-api + ${tomcat.version} + + + org.apache.tomcat + tomcat-coyote + ${tomcat.version} + org.apache.velocity velocity-engine-core @@ -1462,6 +1473,32 @@ + + java24 + + + + org.apache.maven.plugins + maven-toolchains-plugin + ${maven-toolchains-plugin.version} + + + + toolchain + + + + + + + 24 + + + + + + + on-jdk-11-or-12 @@ -1471,15 +1508,6 @@ --no-module-directories - - on-jdk-early-access - - [24,) - - - https://download.java.net/java/early_access/jdk${java.specification.version}/docs/api/ - - diff --git a/wicket-core/pom.xml b/wicket-core/pom.xml index c961759296d..446176d547a 100644 --- a/wicket-core/pom.xml +++ b/wicket-core/pom.xml @@ -152,6 +152,14 @@ org.apache.wicket.validation.validator;-noimport:=true com.github.openjson openjson + + org.apache.tomcat + tomcat-api + + + org.apache.tomcat + tomcat-coyote + org.apache.wicket wicket-request diff --git a/wicket-core/src/main/java/module-info.java b/wicket-core/src/main/java/module-info.java index e7bd7b3a4b0..b9c9598b00a 100644 --- a/wicket-core/src/main/java/module-info.java +++ b/wicket-core/src/main/java/module-info.java @@ -30,8 +30,10 @@ requires org.danekja.jdk.serializable.functional; requires com.github.openjson; requires static org.bouncycastle.provider; + requires org.apache.tomcat.coyote; + requires org.apache.tomcat.api; - provides org.apache.wicket.IInitializer with org.apache.wicket.Initializer; + provides org.apache.wicket.IInitializer with org.apache.wicket.Initializer; provides org.apache.wicket.resource.FileSystemPathService with org.apache.wicket.resource.FileSystemJarPathService; uses org.apache.wicket.IInitializer; diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/html/form/Form.java b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/Form.java index 0a9326ad7cd..c6a4a3facad 100644 --- a/wicket-core/src/main/java/org/apache/wicket/markup/html/form/Form.java +++ b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/Form.java @@ -29,6 +29,7 @@ import org.apache.commons.fileupload2.core.FileUploadByteCountLimitException; import org.apache.commons.fileupload2.core.FileUploadSizeException; import org.apache.commons.fileupload2.core.FileUploadFileCountLimitException; +import org.apache.wicket.Application; import org.apache.wicket.Component; import org.apache.wicket.IGenericComponent; import org.apache.wicket.IRequestListener; @@ -50,6 +51,7 @@ import org.apache.wicket.model.Model; import org.apache.wicket.protocol.http.servlet.MultipartServletWebRequest; import org.apache.wicket.protocol.http.servlet.ServletWebRequest; +import org.apache.wicket.protocol.http.servlet.TomcatUploadProgressListenerFactory; import org.apache.wicket.request.IRequestParameters; import org.apache.wicket.request.Request; import org.apache.wicket.request.Response; @@ -278,6 +280,11 @@ public void component(final Component component, final IVisit visit) /** True if the form has enctype of multipart/form-data */ private short multiPart = 0; + /** + * The ID of the file upload. + */ + private String uploadId; + /** * A user has explicitly called {@link #setMultiPart(boolean)} with value {@code true} forcing * it to be true @@ -1451,7 +1458,7 @@ protected boolean handleMultiPart() { ServletWebRequest request = (ServletWebRequest)getRequest(); final MultipartServletWebRequest multipartWebRequest = request.newMultipartWebRequest( - getMaxSize(), getPage().getId()); + getMaxSize(), getUploadId()); multipartWebRequest.setFileMaxSize(getFileMaxSize()); multipartWebRequest.setFileCountMax(getFileCountMax()); multipartWebRequest.parseFileParts(); @@ -1477,6 +1484,37 @@ protected boolean handleMultiPart() return true; } + /** + * + * @return The upload ID. + */ + public final String getUploadId() + { + if (uploadId != null) + { + return uploadId; + } + uploadId = computeUploadId(getPage()); + return uploadId; + } + + /** + * Computes the upload ID. + * + * @param page The {@link Page} + * @return the upload ID. + */ + public static String computeUploadId(Page page) { + if (Application.get().getApplicationSettings().isUseTomcatNativeFileUpload()) { + String uploadId = TomcatUploadProgressListenerFactory.getUploadId(); + if (uploadId != null) { + return uploadId; + } + throw new WicketRuntimeException("If you are using Tomcat for uploading files you should have registered a TomcatUploadProgressListenerFactory"); + } + return page.getId(); + } + /** * The default message may look like ".. may not exceed 10240 Bytes..". Which is ok, but * sometimes you may want something like "10KB". By subclassing this method you may replace @@ -1669,6 +1707,9 @@ private void adjustNestedTagName(ComponentTag tag) { */ protected CharSequence getActionUrl() { + if (isMultiPart()) { + return urlForListener(new PageParameters().add("uploadId", getUploadId())); + } return urlForListener(new PageParameters()); } diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/FileUploadField.java b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/FileUploadField.java index 3fc87d9b8c8..d39abc2ae8c 100644 --- a/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/FileUploadField.java +++ b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/FileUploadField.java @@ -159,6 +159,10 @@ protected List convertValue(String[] value) throws ConversionExcepti return getFileUploads(); } + public String getUploadId() { + return getMarkupId(); + } + @Override public boolean isMultiPart() { diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/FileUploadToResourceField.java b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/FileUploadToResourceField.java index fbff4d91e0a..b6de730a80f 100644 --- a/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/FileUploadToResourceField.java +++ b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/FileUploadToResourceField.java @@ -27,6 +27,8 @@ import java.util.Objects; import java.util.UUID; import org.apache.commons.io.IOUtils; +import org.apache.wicket.Application; +import org.apache.wicket.WicketRuntimeException; import org.apache.wicket.ajax.AbstractDefaultAjaxBehavior; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.AjaxUtils; @@ -38,6 +40,7 @@ import org.apache.wicket.markup.html.form.upload.FileUploadField; import org.apache.wicket.model.IModel; import org.apache.wicket.model.Model; +import org.apache.wicket.protocol.http.servlet.TomcatUploadProgressListenerFactory; import org.apache.wicket.request.Request; import org.apache.wicket.request.cycle.RequestCycle; import org.apache.wicket.request.mapper.parameter.PageParameters; @@ -59,6 +62,8 @@ public abstract class FileUploadToResourceField extends FileUploadField { private static final Logger LOGGER = LoggerFactory.getLogger(FileUploadToResourceField.class); + private String uploadId; + /** * Info regarding an upload. */ @@ -180,7 +185,7 @@ public List getObject() // at this point files were stored at the server side by resource // UploadFieldId acts as a discriminator at application level // so that uploaded files are isolated. - uploadInfo.setFile(fileManager().getFile(getUploadFieldId(), uploadInfo.clientFileName)); + uploadInfo.setFile(fileManager().getFile(getUploadId(), uploadInfo.clientFileName)); } return fileInfos; } @@ -194,9 +199,9 @@ public void setObject(List object) protected abstract IUploadsFileManager fileManager(); /* - This is an application unique ID assigned to upload field. + This is an application wide unique ID assigned to upload field. */ - protected abstract String getUploadFieldId(); + protected abstract String getUploadId(); protected abstract List getFileUploadInfos(); } @@ -235,9 +240,9 @@ protected IUploadsFileManager fileManager() { } @Override - protected String getUploadFieldId() + protected String getUploadId() { - return FileUploadToResourceField.this.getMarkupId(); + return FileUploadToResourceField.this.getUploadId(); } @Override @@ -357,13 +362,13 @@ protected String generateAUniqueApplicationWiseId() return "WRFUF_" + UUID.randomUUID().toString().replace("-", "_"); } - @Override public void renderHead(IHeaderResponse response) { CoreLibrariesContributor.contributeAjax(getApplication(), response); response.render(JavaScriptHeaderItem.forReference(JS)); JSONObject jsonObject = new JSONObject(); jsonObject.put("inputName", getMarkupId()); + jsonObject.put("uploadId", getUploadId()); jsonObject.put("resourceUrl", urlFor(getFileUploadResourceReference(), new PageParameters()).toString()); jsonObject.put("ajaxCallBackUrl", ajaxBehavior.getCallbackUrl()); jsonObject.put("maxSize", getMaxSize().bytes()); @@ -382,6 +387,33 @@ public void renderHead(IHeaderResponse response) { + getClientSideUploadErrorCallBack() + ");")); } + /** + * @return the unique upload ID. + */ + public final String getUploadId() { + if (uploadId != null) + { + return uploadId; + } + uploadId = computeUploadId(); + return uploadId; + } + + /** + * Comoputes the upload ID. + * @return + */ + private String computeUploadId() { + if (Application.get().getApplicationSettings().isUseTomcatNativeFileUpload()) { + String uploadId = TomcatUploadProgressListenerFactory.getUploadId(); + if (uploadId != null) { + return uploadId; + } + throw new WicketRuntimeException("If you are using Tomcat for uploading files you should have registered a TomcatUploadProgressListenerFactory"); + } + return getMarkupId(); + } + /** * Sets maximum size of each file in upload request. * diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/FileUploadToResourceField.js b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/FileUploadToResourceField.js index 815e66912f6..85b0621f892 100644 --- a/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/FileUploadToResourceField.js +++ b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/upload/resource/FileUploadToResourceField.js @@ -27,8 +27,9 @@ { this.settings = settings; this.inputName = settings.inputName; + this.uploadId = settings.uploadId; this.input = document.getElementById(this.inputName); - this.resourceUrl = settings.resourceUrl + "?uploadId=" + this.inputName + "&maxSize=" + this.settings.maxSize; + this.resourceUrl = settings.resourceUrl + "?uploadId=" + this.uploadId + "&maxSize=" + this.settings.maxSize; if (this.settings.fileMaxSize != null) { this.resourceUrl = this.resourceUrl + "&fileMaxSize=" + this.settings.fileMaxSize; } diff --git a/wicket-core/src/main/java/org/apache/wicket/protocol/http/BufferedHttpServletResponse.java b/wicket-core/src/main/java/org/apache/wicket/protocol/http/BufferedHttpServletResponse.java index a7a971b496d..16e2f8d5f4c 100644 --- a/wicket-core/src/main/java/org/apache/wicket/protocol/http/BufferedHttpServletResponse.java +++ b/wicket-core/src/main/java/org/apache/wicket/protocol/http/BufferedHttpServletResponse.java @@ -81,7 +81,7 @@ public BufferedHttpServletResponse(HttpServletResponse realResponse) } /** - * @see jakarta.servlet.http.HttpServletResponse#addCookie(javax.servlet.http.Cookie) + * @see jakarta.servlet.http.HttpServletResponse#addCookie(jakarta.servlet.http.Cookie) */ @Override public void addCookie(Cookie cookie) @@ -158,6 +158,18 @@ public void sendRedirect(String location) throws IOException redirect = location; } + @Override + public void sendRedirect(String location, int sc, boolean clearBuffer) throws IOException { + isOpen(); + realResponse.sendRedirect(location); + } + + @Override + public void sendEarlyHints() { + isOpen(); + realResponse.sendEarlyHints(); + } + /** * @return The redirect url */ diff --git a/wicket-core/src/main/java/org/apache/wicket/protocol/http/WebApplication.java b/wicket-core/src/main/java/org/apache/wicket/protocol/http/WebApplication.java index 687ae3bfb95..b0a661181ee 100644 --- a/wicket-core/src/main/java/org/apache/wicket/protocol/http/WebApplication.java +++ b/wicket-core/src/main/java/org/apache/wicket/protocol/http/WebApplication.java @@ -24,6 +24,8 @@ import java.util.Locale; import java.util.function.Function; +import org.apache.commons.fileupload2.core.FileItemFactory; +import org.apache.commons.fileupload2.core.FileUploadException; import org.apache.wicket.Application; import org.apache.wicket.Page; import org.apache.wicket.RuntimeConfigurationType; @@ -56,8 +58,10 @@ import org.apache.wicket.markup.resolver.AutoLinkResolver; import org.apache.wicket.protocol.http.servlet.AbstractRequestWrapperFactory; import org.apache.wicket.protocol.http.servlet.FilterFactoryManager; +import org.apache.wicket.protocol.http.servlet.MultipartServletWebRequest; import org.apache.wicket.protocol.http.servlet.ServletWebRequest; import org.apache.wicket.protocol.http.servlet.ServletWebResponse; +import org.apache.wicket.protocol.http.servlet.TomcatNativeMultipartServletWebRequestImpl; import org.apache.wicket.request.IRequestHandler; import org.apache.wicket.request.IRequestMapper; import org.apache.wicket.request.Request; @@ -79,6 +83,7 @@ import org.apache.wicket.util.file.IFileCleaner; import org.apache.wicket.util.file.Path; import org.apache.wicket.util.lang.Args; +import org.apache.wicket.util.lang.Bytes; import org.apache.wicket.util.lang.PackageName; import org.apache.wicket.util.string.Strings; import org.apache.wicket.util.watch.IModificationWatcher; @@ -561,6 +566,23 @@ public final void addResourceReplacement(CssResourceReference base, */ public WebRequest newWebRequest(HttpServletRequest servletRequest, final String filterPath) { + if (getApplicationSettings().isUseTomcatNativeFileUpload()) + { + return new ServletWebRequest(servletRequest, filterPath) + { + @Override + public MultipartServletWebRequest newMultipartWebRequest(Bytes maxSize, String upload) throws FileUploadException + { + return new TomcatNativeMultipartServletWebRequestImpl(getContainerRequest(), getFilterPrefix(), maxSize); + } + + @Override + public MultipartServletWebRequest newMultipartWebRequest(Bytes maxSize, String upload, FileItemFactory factory) throws FileUploadException + { + return new TomcatNativeMultipartServletWebRequestImpl(getContainerRequest(), getFilterPrefix(), maxSize); + } + }; + } return new ServletWebRequest(servletRequest, filterPath); } diff --git a/wicket-core/src/main/java/org/apache/wicket/protocol/http/mock/MockHttpServletResponse.java b/wicket-core/src/main/java/org/apache/wicket/protocol/http/mock/MockHttpServletResponse.java index 684dde00a88..9b122ede07a 100755 --- a/wicket-core/src/main/java/org/apache/wicket/protocol/http/mock/MockHttpServletResponse.java +++ b/wicket-core/src/main/java/org/apache/wicket/protocol/http/mock/MockHttpServletResponse.java @@ -554,6 +554,17 @@ public void sendRedirect(String location) throws IOException status = HttpServletResponse.SC_FOUND; } + @Override + public void sendRedirect(String location, int sc, boolean clearBuffer) throws IOException { + redirectLocation = location; + status = sc; + } + + @Override + public void sendEarlyHints() { + + } + /** * Method ignored. * 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 6ae48f25a6e..6ba2070a63c 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 @@ -386,10 +386,15 @@ protected boolean wantUploadProgressUpdates() * @param totalBytes */ protected void onUploadStarted(int totalBytes) + { + onUploadStarted(getContainerRequest(), upload, totalBytes); + } + + public static void onUploadStarted(HttpServletRequest request, String upload, long totalBytes) { UploadInfo info = new UploadInfo(totalBytes); - setUploadInfo(getContainerRequest(), upload, info); + setUploadInfo(request, upload, info); } /** @@ -400,15 +405,18 @@ protected void onUploadStarted(int totalBytes) */ protected void onUploadUpdate(int bytesUploaded, int total) { - HttpServletRequest request = getContainerRequest(); + onUploadUpdate(getContainerRequest(), upload, bytesUploaded, total); + } + + public static void onUploadUpdate(HttpServletRequest request, String upload, long bytesUploaded, long total) + { UploadInfo info = getUploadInfo(request, upload); if (info == null) { throw new IllegalStateException( - "could not find UploadInfo object in session which should have been set when uploaded started"); + "could not find UploadInfo object in session which should have been set when uploaded started"); } info.setBytesUploaded(bytesUploaded); - setUploadInfo(request, upload, info); } @@ -420,6 +428,11 @@ protected void onUploadCompleted() clearUploadInfo(getContainerRequest(), upload); } + public static void onUploadCompleted(HttpServletRequest request, String upload) + { + clearUploadInfo(request, upload); + } + /** * An {@link InputStream} that updates total number of bytes read * diff --git a/wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/ServletPartFileItem.java b/wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/ServletPartFileItem.java index e09f7dd07f0..bc83c2cb0f1 100644 --- a/wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/ServletPartFileItem.java +++ b/wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/ServletPartFileItem.java @@ -16,7 +16,6 @@ */ package org.apache.wicket.protocol.http.servlet; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -65,6 +64,10 @@ public InputStream getInputStream() throws IOException return part.getInputStream(); } + public Part getPart() { + return part; + } + @Override public String getContentType() { diff --git a/wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/TomcatNativeMultipartServletWebRequestImpl.java b/wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/TomcatNativeMultipartServletWebRequestImpl.java new file mode 100644 index 00000000000..aab1d0bcdd9 --- /dev/null +++ b/wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/TomcatNativeMultipartServletWebRequestImpl.java @@ -0,0 +1,359 @@ +/* + * 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.protocol.http.servlet; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.commons.fileupload2.core.FileItem; +import org.apache.commons.fileupload2.core.FileItemFactory; +import org.apache.commons.fileupload2.core.FileUploadByteCountLimitException; +import org.apache.commons.fileupload2.core.FileUploadException; +import org.apache.commons.fileupload2.jakarta.servlet5.JakartaServletFileUpload; +import org.apache.wicket.Application; +import org.apache.wicket.WicketRuntimeException; +import org.apache.wicket.util.lang.Args; +import org.apache.wicket.util.lang.Bytes; +import org.apache.wicket.util.string.StringValue; +import org.apache.wicket.util.value.ValueMap; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.Part; + +/** + * Servlet-specific WebRequest subclass for multipart content uploads. Aimed to be used with tomcat 11+. This in + * combination with {@link TomcatUploadProgressListenerFactory} and the setting {@link org.apache.wicket.settings.ApplicationSettings#setUseTomcatNativeFileUpload(boolean)} + * allows to use tomcat's native multipart processing with progress reporting. + * + * @author Jonathan Locke + * @author Eelco Hillenius + * @author Cameron Braid + * @author Ate Douma + * @author Igor Vaynberg (ivaynberg) + * @author Ernesto Reinaldo Barreiro (reiern70) + */ +public class TomcatNativeMultipartServletWebRequestImpl extends MultipartServletWebRequest +{ + /** Map of file items. */ + private final Map> files; + + /** Map of parameters. */ + private final ValueMap parameters; + + /** + * Constructor + * + * @param request + * the servlet request + * @param filterPrefix + * prefix to wicket filter mapping + * @param maxSize + * the maximum size allowed for this request + */ + public TomcatNativeMultipartServletWebRequestImpl(HttpServletRequest request, String filterPrefix, + Bytes maxSize) + { + super(request, filterPrefix); + + parameters = new ValueMap(); + files = new HashMap<>(); + + // Check that request is multipart + final boolean isMultipart = JakartaServletFileUpload.isMultipartContent(request); + if (!isMultipart) + { + throw new IllegalStateException( + "ServletRequest does not contain multipart content. One possible solution is to explicitly call Form.setMultipart(true), Wicket tries its best to auto-detect multipart forms but there are certain situations where it cannot."); + } + + setMaxSize(maxSize); + } + + @Override + public void parseFileParts() throws FileUploadException + { + HttpServletRequest request = getContainerRequest(); + + // The encoding that will be used to decode the string parameters + // It should NOT be null at this point, but it may be + // especially if the older Servlet API 2.2 is used + String encoding = request.getCharacterEncoding(); + + // The encoding can also be null when using multipart/form-data encoded forms. + // In that case we use the [application-encoding] which we always demand using + // the attribute 'accept-encoding' in wicket forms. + if (encoding == null) + { + encoding = Application.get().getRequestCycleSettings().getResponseRequestEncoding(); + } + + + List items = readServletParts(request); + + // Loop through items + for (final FileItem item : items) + { + // Get next item + // If item is a form field + if (item.isFormField()) + { + // Set parameter value + final String value; + if (encoding != null) + { + try + { + value = item.getString(Charset.forName(encoding)); + } + catch (IOException e) + { + throw new WicketRuntimeException(e); + } + } + else + { + value = item.getString(); + } + + addParameter(item.getFieldName(), value); + } + else + { + List fileItems = files.get(item.getFieldName()); + if (fileItems == null) + { + fileItems = new ArrayList<>(); + files.put(item.getFieldName(), fileItems); + } + // Add to file list + fileItems.add(item); + } + } + } + + /** + * Reads the uploads' parts by using Servlet APIs. This is meant to be used with tomcat 11+; + * + * Note: Mind that in to get file upload with prpgres working you need to: + * + * 1) register a {@link TomcatUploadProgressListenerFactory} + * 2) set to true {@link org.apache.wicket.settings.ApplicationSettings#setUseTomcatNativeFileUpload} + * + * @param request + * The http request with the upload data + * @return A list of {@link FileItem}s + * @throws FileUploadException + */ + private List readServletParts(HttpServletRequest request) throws FileUploadException + { + List itemsFromParts = new ArrayList<>(); + try + { + Collection parts = request.getParts(); + if (parts != null) + { + for (Part part : parts) + { + FileItem fileItem = new ServletPartFileItem(part) { + @Override + public ServletPartFileItem write(Path path) throws IOException { + // we need to override this because supper method only uses file name and file is + // not stored. + getPart().write(path.toFile().getAbsolutePath()); + return this; + } + }; + itemsFromParts.add(fileItem); + } + } + } catch (IOException | ServletException e) + { + throw new FileUploadException("An error occurred while reading the upload parts", e); + } + return itemsFromParts; + } + + /** + * Adds a parameter to the parameters value map + * + * @param name + * parameter name + * @param value + * parameter value + */ + private void addParameter(final String name, final String value) + { + final String[] currVal = (String[])parameters.get(name); + + String[] newVal; + + if (currVal != null) + { + newVal = new String[currVal.length + 1]; + System.arraycopy(currVal, 0, newVal, 0, currVal.length); + newVal[currVal.length] = value; + } + else + { + newVal = new String[] { value }; + + } + + parameters.put(name, newVal); + } + + /** + * @return Returns the files. + */ + @Override + public Map> getFiles() + { + return files; + } + + /** + * Gets the file that was uploaded using the given field name. + * + * @param fieldName + * the field name that was used for the upload + * @return the upload with the given field name + */ + @Override + public List getFile(final String fieldName) + { + return files.get(fieldName); + } + + @Override + protected Map> generatePostParameters() + { + Map> res = new HashMap<>(); + for (Map.Entry entry : parameters.entrySet()) + { + String key = entry.getKey(); + String[] val = (String[])entry.getValue(); + if (val != null && val.length > 0) + { + List items = new ArrayList<>(); + for (String s : val) + { + items.add(StringValue.valueOf(s)); + } + res.put(key, items); + } + } + return res; + } + + + @Override + 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> entry : files.entrySet()) + { + List fileItems = entry.getValue(); + for (FileItem fileItem : fileItems) + { + if (fileMaxSize != null && fileItem.getSize() > fileMaxSize.bytes()) + { + String fieldName = entry.getKey(); + FileUploadException fslex = new FileUploadByteCountLimitException("The field '" + + fieldName + "' exceeds its maximum permitted size of '" + + maxSize + "' characters.", fileItem.getSize(), fileMaxSize.bytes(), fileItem.getName(), fieldName); + throw fslex; + } + } + } + return this; + } + + @Override + public MultipartServletWebRequest newMultipartWebRequest(Bytes maxSize, String upload, FileItemFactory factory) + throws FileUploadException + { + return this; + } + + private static final String SESSION_KEY = TomcatNativeMultipartServletWebRequestImpl.class.getName(); + + private static String getSessionKey(String upload) + { + return SESSION_KEY + ":" + upload; + } + + /** + * Retrieves {@link UploadInfo} from session, null if not found. + * + * @param req + * http servlet request, not null + * @param upload + * upload identifier + * @return {@link UploadInfo} object from session, or null if not found + */ + public static UploadInfo getUploadInfo(final HttpServletRequest req, String upload) + { + Args.notNull(req, "req"); + return (UploadInfo)req.getSession().getAttribute(getSessionKey(upload)); + } + + /** + * Sets the {@link UploadInfo} object into session. + * + * @param req + * http servlet request, not null + * @param upload + * upload identifier + * @param uploadInfo + * {@link UploadInfo} object to be put into session, not null + */ + public static void setUploadInfo(final HttpServletRequest req, String upload, + final UploadInfo uploadInfo) + { + Args.notNull(req, "req"); + Args.notNull(upload, "upload"); + Args.notNull(uploadInfo, "uploadInfo"); + req.getSession().setAttribute(getSessionKey(upload), uploadInfo); + } + + /** + * Clears the {@link UploadInfo} object from session if one exists. + * + * @param req + * http servlet request, not null + * @param upload + * upload identifier + */ + public static void clearUploadInfo(final HttpServletRequest req, String upload) + { + Args.notNull(req, "req"); + Args.notNull(upload, "upload"); + req.getSession().removeAttribute(getSessionKey(upload)); + } + +} diff --git a/wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/TomcatUploadProgressListenerFactory.java b/wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/TomcatUploadProgressListenerFactory.java new file mode 100644 index 00000000000..1150b18a5f4 --- /dev/null +++ b/wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/TomcatUploadProgressListenerFactory.java @@ -0,0 +1,156 @@ +/* + * 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.protocol.http.servlet; + +import java.util.Optional; +import java.util.concurrent.atomic.AtomicLong; +import org.apache.tomcat.util.http.fileupload.ProgressListener; +import org.apache.tomcat.util.http.fileupload.ProgressListenerFactory; +import org.apache.wicket.Application; +import org.apache.wicket.request.Url; +import org.apache.wicket.util.lang.Args; +import jakarta.servlet.http.HttpServletRequest; + +/** + * A {@link ProgressListenerFactory} that allows reporting upload progress but uses tomcat native multipart machinery. + */ +public class TomcatUploadProgressListenerFactory implements ProgressListenerFactory +{ + /** + * Interface used to generate upload IDs. These IDs connect Wicket UI with tomcat progress reporting + */ + public interface IUploadIdGenerator + { + /** + * @return The unique ID for the upload. + */ + String newUploadId(); + } + + private static class AppUploadIdGenerator implements IUploadIdGenerator + { + + private static final AppUploadIdGenerator instance = new AppUploadIdGenerator(); + + public static AppUploadIdGenerator getInstance() + { + return instance; + } + + private final AtomicLong counter = new AtomicLong(); + + private AppUploadIdGenerator() + { + } + + @Override + public String newUploadId() { + return "upload-" + counter.incrementAndGet(); + } + } + + /** + * Progress listener to be called by Tomcat to report multipart (file upload) progress.- + */ + public static class WicketProgressListener implements ProgressListener + { + + private final String uploadId; + private final HttpServletRequest servletRequest; + private final long totalBytes; + + private WicketProgressListener(String uploadId, HttpServletRequest servletRequest) + { + Args.notEmpty(uploadId, "uploadId"); + this.uploadId = uploadId; + Args.notNull(servletRequest, "servletRequest"); + this.servletRequest = servletRequest; + this.totalBytes = servletRequest.getContentLength(); + } + + @Override + public void uploadStarted() + { + MultipartServletWebRequestImpl.onUploadStarted(servletRequest, this.uploadId, this.totalBytes); + } + + @Override + public void update(long pBytesRead, long pContentLength, int pItems) + { + MultipartServletWebRequestImpl.onUploadUpdate(servletRequest, uploadId, pBytesRead, pContentLength); + } + + @Override + public void uploadFinished() + { + MultipartServletWebRequestImpl.onUploadCompleted(servletRequest, this.uploadId); + } + } + + private static IUploadIdGenerator iUploadIdGenerator = AppUploadIdGenerator.getInstance(); + + + public TomcatUploadProgressListenerFactory() + { + // constructor for reflection-based instantiation + } + + + @Override + public ProgressListener newProgressListener(HttpServletRequest servletRequest) { + // there is no need to check if we are in multipart request + // we are because tomcat will only call this in the context of a + // multipart request. + if (wantUploadProgressUpdates()) + { + // we extract the uploadId from the request + Url url = Url.parse(servletRequest.getRequestURL() + "?" + servletRequest.getQueryString()); + Optional queryParameter = url.getQueryParameters().stream().filter( + queryParameter1 -> queryParameter1.getName().equals("uploadId")).findFirst(); + if (queryParameter.isPresent()) + { + String uploadId = queryParameter.get().getValue(); + return new WicketProgressListener(uploadId, servletRequest); + } + } + return null; + } + + protected boolean wantUploadProgressUpdates() + { + return Application.get().getApplicationSettings().isUploadProgressUpdatesEnabled(); + } + + public static String getUploadId() { + if (Application.get().getApplicationSettings().isUseTomcatNativeFileUpload()) + { + return iUploadIdGenerator.newUploadId(); + } + return null; + } + + /** + * Allows setting the {@link IUploadIdGenerator} + * + * @param iUploadIdGenerator {@link IUploadIdGenerator} + */ + public static void setUploadIdGenerator(IUploadIdGenerator iUploadIdGenerator) + { + Args.notNull(iUploadIdGenerator, "iUploadIdGenerator"); + TomcatUploadProgressListenerFactory.iUploadIdGenerator = iUploadIdGenerator; + } +} diff --git a/wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/UploadInfo.java b/wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/UploadInfo.java index c52831659d5..bada6573fc6 100644 --- a/wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/UploadInfo.java +++ b/wicket-core/src/main/java/org/apache/wicket/protocol/http/servlet/UploadInfo.java @@ -39,6 +39,7 @@ public class UploadInfo implements IClusterable /** * @param totalBytes + * @deprecated We need to keep it for backwards compatibility */ public UploadInfo(final int totalBytes) { @@ -46,6 +47,15 @@ public UploadInfo(final int totalBytes) this.totalBytes = totalBytes; } + /** + * @param totalBytes + */ + public UploadInfo(final long totalBytes) + { + timeStarted = System.currentTimeMillis(); + this.totalBytes = totalBytes; + } + /** * @return bytes uploaded so far */ @@ -56,8 +66,8 @@ public long getBytesUploaded() /** * Sets bytes uploaded so far - * - * @param bytesUploaded + * + * @param bytesUploaded The number of bytes uploaded */ public void setBytesUploaded(final long bytesUploaded) { @@ -65,7 +75,7 @@ public void setBytesUploaded(final long bytesUploaded) } /** - * @return human readable string of bytes uploaded so far + * @return human-readable string of bytes uploaded so far */ public String getBytesUploadedString() { diff --git a/wicket-core/src/main/java/org/apache/wicket/settings/ApplicationSettings.java b/wicket-core/src/main/java/org/apache/wicket/settings/ApplicationSettings.java index 3d5141cc381..b5fa0757700 100644 --- a/wicket-core/src/main/java/org/apache/wicket/settings/ApplicationSettings.java +++ b/wicket-core/src/main/java/org/apache/wicket/settings/ApplicationSettings.java @@ -63,6 +63,8 @@ public class ApplicationSettings private boolean uploadProgressUpdatesEnabled = false; + private boolean useTomcatNativeFileUpload = false; + private IFeedbackMessageFilter feedbackMessageCleanupFilter = new DefaultCleanupFeedbackMessageFilter(); /** @@ -126,6 +128,16 @@ public boolean isUploadProgressUpdatesEnabled() return uploadProgressUpdatesEnabled; } + /** + * Gets whether wicket is using Tomcat 11+ native upload machinery or not. + * + * @return if true, Wicket will use tomcat native upload machinery + */ + public boolean isUseTomcatNativeFileUpload() + { + return useTomcatNativeFileUpload; + } + /** * Sets the access denied page class. The class must be bookmarkable and must extend Page. * @@ -224,6 +236,19 @@ public ApplicationSettings setUploadProgressUpdatesEnabled(boolean uploadProgres return this; } + + /** + * Sets whether wicket should use Tomcat (11+) native file upload + * + * @param useTomcatNativeFileUpload + * if true, Wicket will use tomcat native file upload + * @return {@code this} object for chaining + */ + public ApplicationSettings setUseTomcatNativeFileUpload(boolean useTomcatNativeFileUpload) { + this.useTomcatNativeFileUpload = useTomcatNativeFileUpload; + return this; + } + /** * Throws an IllegalArgumentException if the given class is not a subclass of Page. * diff --git a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/AjaxFileDropBehavior.java b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/AjaxFileDropBehavior.java index b8a76b0020a..47ae6898a25 100644 --- a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/AjaxFileDropBehavior.java +++ b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/AjaxFileDropBehavior.java @@ -31,6 +31,7 @@ import org.apache.wicket.core.util.string.CssUtils; import org.apache.wicket.markup.head.IHeaderResponse; import org.apache.wicket.markup.head.JavaScriptHeaderItem; +import org.apache.wicket.markup.html.form.Form; import org.apache.wicket.markup.html.form.upload.FileUpload; import org.apache.wicket.protocol.http.servlet.MultipartServletWebRequest; import org.apache.wicket.protocol.http.servlet.ServletWebRequest; @@ -121,7 +122,7 @@ protected void onEvent(AjaxRequestTarget target) { ServletWebRequest request = (ServletWebRequest)getComponent().getRequest(); final MultipartServletWebRequest multipartWebRequest = request - .newMultipartWebRequest(getMaxSize(), getComponent().getPage().getId()); + .newMultipartWebRequest(getMaxSize(), Form.computeUploadId(getComponent().getPage())); multipartWebRequest.setFileMaxSize(getFileMaxSize()); multipartWebRequest.setFileCountMax(getFileCountMax()); multipartWebRequest.parseFileParts(); diff --git a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/form/upload/UploadProgressBar.java b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/form/upload/UploadProgressBar.java index 52e4d29a553..16b7c44dbab 100644 --- a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/form/upload/UploadProgressBar.java +++ b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/form/upload/UploadProgressBar.java @@ -21,6 +21,7 @@ import org.apache.wicket.Application; import org.apache.wicket.IInitializer; import org.apache.wicket.MarkupContainer; +import org.apache.wicket.WicketRuntimeException; import org.apache.wicket.core.request.handler.IPartialPageRequestHandler; import org.apache.wicket.markup.head.CssHeaderItem; import org.apache.wicket.markup.head.IHeaderResponse; @@ -259,8 +260,12 @@ public void renderHead(final IHeaderResponse response) final String status = new StringResourceModel(RESOURCE_STARTING, this, null).getString(); - CharSequence url = form != null ? urlFor(ref, UploadStatusResource.newParameter(getPage().getId())) : - urlFor(ref, UploadStatusResource.newParameter(uploadField.getMarkupId())); + if (form == null && uploadField == null) { + throw new WicketRuntimeException("Either form or uploadField must be set"); + } + + CharSequence url = (form != null && form.isMultiPart()) ? urlFor(ref, UploadStatusResource.newParameter(form.getUploadId())) : + urlFor(ref, UploadStatusResource.newParameter(uploadField.getUploadId())); StringBuilder builder = new StringBuilder(128); Formatter formatter = new Formatter(builder); diff --git a/wicket-migration/src/test/java/org/apache/wicket/migration/MigrateToWicket10Test.java b/wicket-migration/src/test/java/org/apache/wicket/migration/MigrateToWicket10Test.java index 6bce2f1c571..451d50c90af 100644 --- a/wicket-migration/src/test/java/org/apache/wicket/migration/MigrateToWicket10Test.java +++ b/wicket-migration/src/test/java/org/apache/wicket/migration/MigrateToWicket10Test.java @@ -34,6 +34,7 @@ class MigrateToWicket10Test implements RewriteTest { @Override + @Disabled public void defaults(RecipeSpec spec) { spec .parser(JavaParser.fromJavaVersion() @@ -46,6 +47,7 @@ public void defaults(RecipeSpec spec) { } @Test + @Disabled void migrateImports() { //language=java rewriteRun(