diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiContentAccess.java b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiContentAccess.java index 7a4a23f43f..cfa7f63ce3 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiContentAccess.java +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiContentAccess.java @@ -31,6 +31,8 @@ import jakarta.ws.rs.HttpMethod; import jakarta.ws.rs.core.MultivaluedHashMap; import jakarta.ws.rs.core.MultivaluedMap; import jakarta.ws.rs.core.Response; +import org.springframework.http.ContentDisposition; + import java.net.URI; import java.net.URISyntaxException; import java.util.Collections; @@ -115,7 +117,8 @@ public class StandardNiFiContentAccess implements ContentAccess { // get the file name final String contentDisposition = getHeader(responseHeaders, "content-disposition"); - final String filename = StringUtils.substringBetween(contentDisposition, "filename=\"", "\""); + final ContentDisposition contentDispositionParsed = ContentDisposition.parse(contentDisposition); + final String filename = contentDispositionParsed.getFilename(); // get the content type final String contentType = getHeader(responseHeaders, "content-type"); diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowFileQueueResource.java b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowFileQueueResource.java index 2f87914ee3..bd9615abb9 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowFileQueueResource.java +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/FlowFileQueueResource.java @@ -65,6 +65,7 @@ import org.apache.nifi.web.api.entity.Entity; import org.apache.nifi.web.api.entity.FlowFileEntity; import org.apache.nifi.web.api.entity.ListingRequestEntity; import org.apache.nifi.web.api.request.ClientIdParameter; +import org.apache.nifi.web.util.ResponseBuilderUtils; /** * RESTful endpoint for managing a flowfile queue. @@ -276,7 +277,8 @@ public class FlowFileQueueResource extends ApplicationResource { contentType = MediaType.APPLICATION_OCTET_STREAM; } - return generateOkResponse(response).type(contentType).header("Content-Disposition", String.format("attachment; filename=\"%s\"", content.getFilename())).build(); + final Response.ResponseBuilder responseBuilder = generateOkResponse(response).type(contentType); + return ResponseBuilderUtils.setContentDisposition(responseBuilder, content.getFilename()).build(); } /** diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProvenanceEventResource.java b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProvenanceEventResource.java index 966dde9e09..42994012da 100644 --- a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProvenanceEventResource.java +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/api/ProvenanceEventResource.java @@ -63,6 +63,7 @@ import org.apache.nifi.web.api.entity.ReplayLastEventResponseEntity; import org.apache.nifi.web.api.entity.ReplayLastEventSnapshotDTO; import org.apache.nifi.web.api.entity.SubmitReplayRequestEntity; import org.apache.nifi.web.api.request.LongParameter; +import org.apache.nifi.web.util.ResponseBuilderUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -157,7 +158,8 @@ public class ProvenanceEventResource extends ApplicationResource { contentType = MediaType.APPLICATION_OCTET_STREAM; } - return generateOkResponse(response).type(contentType).header("Content-Disposition", String.format("attachment; filename=\"%s\"", content.getFilename())).build(); + final Response.ResponseBuilder responseBuilder = generateOkResponse(response).type(contentType); + return ResponseBuilderUtils.setContentDisposition(responseBuilder, content.getFilename()).build(); } /** @@ -240,7 +242,8 @@ public class ProvenanceEventResource extends ApplicationResource { contentType = MediaType.APPLICATION_OCTET_STREAM; } - return generateOkResponse(response).type(contentType).header("Content-Disposition", String.format("attachment; filename=\"%s\"", content.getFilename())).build(); + final Response.ResponseBuilder responseBuilder = generateOkResponse(response).type(contentType); + return ResponseBuilderUtils.setContentDisposition(responseBuilder, content.getFilename()).build(); } /** diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/ResponseBuilderUtils.java b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/ResponseBuilderUtils.java new file mode 100644 index 0000000000..c628da82d8 --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/util/ResponseBuilderUtils.java @@ -0,0 +1,44 @@ +/* + * 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.nifi.web.util; + +import jakarta.ws.rs.core.Response; +import org.springframework.http.ContentDisposition; +import org.springframework.http.HttpHeaders; + +import java.nio.charset.StandardCharsets; + +/** + * HTTP Response Builder Utilities + */ +public class ResponseBuilderUtils { + /** + * Set Content-Disposition Header with filename encoded according to RFC requirements + * + * @param responseBuilder HTTP Response Builder + * @param filename Filename to be encoded for Content-Disposition header + * @return HTTP Response Builder + */ + public static Response.ResponseBuilder setContentDisposition(final Response.ResponseBuilder responseBuilder, final String filename) { + final String disposition = ContentDisposition.attachment() + .filename(filename, StandardCharsets.UTF_8) + .build() + .toString(); + + return responseBuilder.header(HttpHeaders.CONTENT_DISPOSITION, disposition); + } +} diff --git a/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/util/ResponseBuilderUtilsTest.java b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/util/ResponseBuilderUtilsTest.java new file mode 100644 index 0000000000..a00b9b3d56 --- /dev/null +++ b/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/test/java/org/apache/nifi/web/util/ResponseBuilderUtilsTest.java @@ -0,0 +1,58 @@ +/* + * 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.nifi.web.util; + + +import jakarta.ws.rs.core.HttpHeaders; +import jakarta.ws.rs.core.Response; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class ResponseBuilderUtilsTest { + + private static final String FILENAME_ASCII = "image.jpg"; + + private static final String DISPOSITION_ASCII = "attachment; filename=\"=?UTF-8?Q?%s?=\"; filename*=UTF-8''%s".formatted(FILENAME_ASCII, FILENAME_ASCII); + + private static final String FILENAME_SPACED = "image label.jpg"; + + private static final String DISPOSITION_ENCODED = "attachment; filename=\"=?UTF-8?Q?image_label.jpg?=\"; filename*=UTF-8''image%20label.jpg"; + + @Test + void testSetContentDisposition() { + final Response.ResponseBuilder responseBuilder = ResponseBuilderUtils.setContentDisposition(Response.ok(), FILENAME_ASCII); + + try (Response response = responseBuilder.build()) { + final String contentDisposition = response.getHeaderString(HttpHeaders.CONTENT_DISPOSITION); + + assertEquals(DISPOSITION_ASCII, contentDisposition); + } + } + + @Test + void testSetContentDispositionEncoded() { + final Response.ResponseBuilder responseBuilder = ResponseBuilderUtils.setContentDisposition(Response.ok(), FILENAME_SPACED); + + try (Response response = responseBuilder.build()) { + final String contentDisposition = response.getHeaderString(HttpHeaders.CONTENT_DISPOSITION); + + assertEquals(DISPOSITION_ENCODED, contentDisposition); + } + } +}