From b6f00959bc4235109b65a34f72e4eb831b7c6e49 Mon Sep 17 00:00:00 2001 From: Jose Castro Date: Fri, 10 Mar 2023 11:45:09 -0600 Subject: [PATCH] Fix #23603 : Allow users to write code when creating a file - PR2 (#24267) * Implementing SonarQube feedback. * #23603 : Allow users to write code when creating a file * Adding more code changes and Postman tests. * Fixing other Postman Tests. * #23603 : Forbid access to Temp File update method for Anonymous Users. * Implementing SonarQube feedback. * Implementing SonarQube feedback. * #23603 : Creating new branch based on master. * Implementing JS feedback from Manuel Rojas. * Implementing more JS feedback from Manuel Rojas. --- .../Content Resource.postman_collection.json | 29 +- .../curl-test/TempAPI.postman_collection.json | 299 +++++++++++++- .../rest/api/v1/temp/PlainTextFileForm.java | 78 ++++ .../dotcms/rest/api/v1/temp/TempFileAPI.java | 349 ++++++++++------- .../rest/api/v1/temp/TempFileResource.java | 368 ++++++++++++------ .../ext/contentlet/field/edit_field.jsp | 27 +- .../field/edit_file_asset_text_inc.jsp | 87 ++++- 7 files changed, 945 insertions(+), 292 deletions(-) create mode 100644 dotCMS/src/main/java/com/dotcms/rest/api/v1/temp/PlainTextFileForm.java diff --git a/dotCMS/src/curl-test/Content Resource.postman_collection.json b/dotCMS/src/curl-test/Content Resource.postman_collection.json index 86807ce82c0e..73b1ba8aaee1 100644 --- a/dotCMS/src/curl-test/Content Resource.postman_collection.json +++ b/dotCMS/src/curl-test/Content Resource.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "64a156b5-40b4-46ce-8bab-6083e910e48a", + "_postman_id": "50e22041-5236-42d7-8d37-b9a39f0457e3", "name": "Content Resource", "description": "Content Resource test", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", @@ -1763,6 +1763,15 @@ "type": "text" } ], + "body": { + "mode": "raw", + "raw": "{}", + "options": { + "raw": { + "language": "json" + } + } + }, "url": { "raw": "{{serverURL}}/api/v1/workflow/actions/default/fire/UNPUBLISH?inode={{fileInode}}&identifier={{fileId}}", "host": [ @@ -1835,6 +1844,15 @@ "type": "text" } ], + "body": { + "mode": "raw", + "raw": "{}", + "options": { + "raw": { + "language": "json" + } + } + }, "url": { "raw": "{{serverURL}}/api/v1/workflow/actions/default/fire/ARCHIVE?inode={{fileInode}}&identifier={{fileId}}", "host": [ @@ -1907,6 +1925,15 @@ "type": "text" } ], + "body": { + "mode": "raw", + "raw": "{}", + "options": { + "raw": { + "language": "json" + } + } + }, "url": { "raw": "{{serverURL}}/api/v1/workflow/actions/default/fire/DELETE?inode={{fileInode}}&identifier={{fileId}}", "host": [ diff --git a/dotCMS/src/curl-test/TempAPI.postman_collection.json b/dotCMS/src/curl-test/TempAPI.postman_collection.json index 7e5e963a8dbe..2c7427f98c45 100644 --- a/dotCMS/src/curl-test/TempAPI.postman_collection.json +++ b/dotCMS/src/curl-test/TempAPI.postman_collection.json @@ -1,11 +1,242 @@ { "info": { - "_postman_id": "1faea77b-c035-4129-a215-3608487d19b3", + "_postman_id": "b65639a1-cec6-4b6c-b8b6-ab483db33b5b", "name": "TempAPI", + "description": "Verifies that the Temp File API is working as expected. It allows users to create temporary files in the dotCMS assets folder.", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "11174695" + "_exporter_id": "5403727" }, "item": [ + { + "name": "Temp File As Plain Text", + "item": [ + { + "name": "Create Temp File", + "event": [ + { + "listen": "prerequest", + "script": { + "exec": [ + "let randomNumber = Math.floor(Math.random() * 100);", + "pm.collectionVariables.set(\"tempFileName\", randomNumber + \"-test-temp-file.txt\");" + ], + "type": "text/javascript" + } + }, + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Test Temporary File creation HTTP Status must be successful\", function() {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.collectionVariables.set(\"tempFileId\", pm.response.json().tempFiles[0].id);" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "username", + "value": "admin@dotcms.com", + "type": "string" + }, + { + "key": "password", + "value": "admin", + "type": "string" + } + ] + }, + "method": "PUT", + "header": [ + { + "key": "Origin", + "value": "localhost", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"fileName\": \"{{tempFileName}}\",\n \"fileContent\": \"This is the content of the Temporary File.\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v1/temp/id/new", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "temp", + "id", + "new" + ] + } + }, + "response": [] + }, + { + "name": "Update Existing Temp File Content", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Test Temporary File update HTTP Status must be successful\", function() {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"Temporary File ID must be the same\", function() {", + " let tempFileId = pm.collectionVariables.get(\"tempFileId\");", + " pm.expect(pm.response.json().tempFiles[0].id).to.eql(tempFileId, \"An error occurred when checking the temp file ID\");", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "username", + "value": "admin@dotcms.com", + "type": "string" + }, + { + "key": "password", + "value": "admin", + "type": "string" + } + ] + }, + "method": "PUT", + "header": [ + { + "key": "Origin", + "value": "localhost", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"fileName\": \"{{tempFileName}}\",\n \"fileContent\": \"This is the new content of the Temporary File.\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v1/temp/id/{{tempFileId}}", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "temp", + "id", + "{{tempFileId}}" + ] + } + }, + "response": [] + }, + { + "name": "Update Non-Existent Temp File", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Non-Existing Test Temporary File creation HTTP Status must be successful\", function() {", + " pm.response.to.have.status(200);", + "});", + "", + "pm.test(\"New Temporary File ID must NOT match the previous one\", function() {", + " let tempFileId = pm.collectionVariables.get(\"tempFileId\");", + " pm.expect(pm.response.json().tempFiles[0].id).to.not.eql(tempFileId, \"An error occurred when checking different temp file IDs\");", + "});" + ], + "type": "text/javascript" + } + }, + { + "listen": "prerequest", + "script": { + "exec": [ + "let randomNumber = Math.floor(Math.random() * 100);", + "pm.collectionVariables.set(\"tempFileName\", \"new-\" + randomNumber + \"-test-temp-file.txt\");" + ], + "type": "text/javascript" + } + } + ], + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "username", + "value": "admin@dotcms.com", + "type": "string" + }, + { + "key": "password", + "value": "admin", + "type": "string" + } + ] + }, + "method": "PUT", + "header": [ + { + "key": "Origin", + "value": "localhost", + "type": "text" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"fileName\": \"{{tempFileName}}\",\n \"fileContent\": \"Here is some test content.\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v1/temp/id/non-existent-id", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "temp", + "id", + "non-existent-id" + ] + } + }, + "response": [] + } + ], + "description": "This request collection creates Temporary Files provided as plain text instead of binary files." + }, { "name": "Upload Multiple with one wrong file", "event": [ @@ -13,14 +244,36 @@ "listen": "test", "script": { "exec": [ - "var jsonData = pm.response.json();", + "pm.test(\"Checking file names and operation status code\", function () {", + " var jsonData = pm.response.json();", + " let found = false;", + " jsonData.tempFiles.forEach((item) => {", "", - "pm.test(\"File name check\", function () {", - " pm.expect(jsonData.tempFiles[0].fileName).to.eql('Landscape_2009_romantic_country_garden.jpeg');", - " pm.expect(jsonData.tempFiles[1].fileName).to.eql('16475687531_eac8a30914_b.jpeg');", - " pm.expect(jsonData.tempFiles[2].errorCode).to.eql('400');", - "});", + " if (item.fileName == \"Landscape_2009_romantic_country_garden.jpeg\") {", + " found = true;", + " }", + "", + " });", + " pm.expect(found).to.eq(true, \"Expected image 'Landscape_2009_romantic_country_garden.jpeg' was not found.\")", + " found = false;", + " jsonData.tempFiles.forEach((item) => {", "", + " if (item.fileName == \"16475687531_eac8a30914_b.jpeg\") {", + " found = true;", + " }", + "", + " });", + " pm.expect(found).to.eq(true, \"Expected image '16475687531_eac8a30914_b.jpeg' was not found.\")", + " found = false;", + " jsonData.tempFiles.forEach((item) => {", + "", + " if (item.errorCode == \"400\") {", + " found = true;", + " }", + "", + " });", + " pm.expect(found).to.eq(true, \"Expected error code '400' not found.\")", + "});", "" ], "type": "text/javascript" @@ -157,5 +410,35 @@ }, "response": [] } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "tempFileName", + "value": "" + }, + { + "key": "tempFileId", + "value": "" + } ] } \ No newline at end of file diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/temp/PlainTextFileForm.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/temp/PlainTextFileForm.java new file mode 100644 index 000000000000..7f08423392a1 --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/temp/PlainTextFileForm.java @@ -0,0 +1,78 @@ +package com.dotcms.rest.api.v1.temp; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +/** + * Provides all the required information of a File Asset that is being provided as plain text. For instance, this can + * be used by the Temp File Resource to create a file with the specified contents, which is what allows Users to create + * a text file -- i.e, TXT, VTL, JS, CSS, and so on -- directly from the dotCMS back-end. + * + * @author Jose Castro + * @since Feb 22nd, 2023 + */ +@JsonDeserialize(builder = PlainTextFileForm.Builder.class) +public class PlainTextFileForm { + + private final String fileName; + private final String fileContent; + + private PlainTextFileForm(final Builder builder) { + this.fileName = builder.fileName; + this.fileContent = builder.fileContent; + } + + public String fileContent() { + return fileContent; + } + + public String fileName() { + return fileName; + } + + /** + * Allows you to build an instance of the {@link PlainTextFileForm} class. + */ + public static final class Builder { + + @JsonProperty(required = true) + private String fileName; + @JsonProperty(required = true) + private String fileContent; + + /** + * Sets the name of the plain text file. + * + * @param fileName The file name. + * + * @return An instance of the class' builder. + */ + public PlainTextFileForm.Builder fileName(final String fileName) { + this.fileName = fileName; + return this; + } + + /** + * Sets the content of the plain text file. + * + * @param fileContent The file's content. + * + * @return An instance of the class' builder. + */ + public PlainTextFileForm.Builder file(final String fileContent) { + this.fileContent = fileContent; + return this; + } + + /** + * Creates an instance of the {@link PlainTextFileForm} class. + * + * @return The instantiated class. + */ + public PlainTextFileForm build() { + return new PlainTextFileForm(this); + } + + } + +} diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/temp/TempFileAPI.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/temp/TempFileAPI.java index 9e0ca422dd68..5ac958c1b48c 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/temp/TempFileAPI.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/temp/TempFileAPI.java @@ -1,29 +1,8 @@ package com.dotcms.rest.api.v1.temp; -import static com.dotcms.storage.FileMetadataAPIImpl.*; - -import com.dotcms.rest.exception.BadRequestException; -import com.dotcms.storage.FileMetadataAPIImpl; -import java.io.File; -import java.io.FileFilter; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.InetAddress; -import java.net.URL; -import java.nio.file.Files; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.Optional; - -import javax.servlet.http.HttpServletRequest; -import org.xbill.DNS.Address; -import org.xbill.DNS.ExtendedResolver; -import org.xbill.DNS.Resolver; import com.dotcms.http.CircuitBreakerUrl; import com.dotcms.http.CircuitBreakerUrl.Method; +import com.dotcms.rest.exception.BadRequestException; import com.dotcms.util.CloseUtils; import com.dotcms.util.ConversionUtils; import com.dotcms.util.SecurityUtils; @@ -35,7 +14,6 @@ import com.dotmarketing.exception.DotRuntimeException; import com.dotmarketing.exception.DotSecurityException; import com.dotmarketing.util.Config; -import com.dotmarketing.util.DNSUtil; import com.dotmarketing.util.FileUtil; import com.dotmarketing.util.Logger; import com.dotmarketing.util.SecurityLogger; @@ -43,16 +21,38 @@ import com.dotmarketing.util.UtilMethods; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.liferay.portal.model.User; import com.liferay.portal.util.PortalUtil; import com.liferay.portal.util.WebKeys; import com.liferay.util.Encryptor; import com.liferay.util.StringPool; - import io.vavr.control.Try; +import javax.servlet.http.HttpServletRequest; +import java.io.File; +import java.io.FileFilter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import static com.dotcms.storage.FileMetadataAPIImpl.META_TMP; + +/** + * This API allows you to create temporary files in dotCMS. This API is very useful for uploading resources that can be + * safely deleted after a given amount of time, and also used by the File Asset edit mode when a new file is being + * uploaded to the repository. + * + * @author Will Ezell + * @since Jul 8th, 2019 + */ public class TempFileAPI { public static final String TEMP_RESOURCE_MAX_AGE_SECONDS = "TEMP_RESOURCE_MAX_AGE_SECONDS"; @@ -60,24 +60,25 @@ public class TempFileAPI { public static final String TEMP_RESOURCE_ALLOW_NO_REFERER = "TEMP_RESOURCE_ALLOW_NO_REFERER"; public static final String TEMP_RESOURCE_MAX_FILE_SIZE = "TEMP_RESOURCE_MAX_FILE_SIZE"; public static final String TEMP_RESOURCE_MAX_FILE_SIZE_ANONYMOUS = "TEMP_RESOURCE_MAX_FILE_SIZE_ANONYMOUS"; + public static final String MAX_FILE_LENGTH_PARAM = "maxFileLength"; public static final String TEMP_RESOURCE_ENABLED = "TEMP_RESOURCE_ENABLED"; public static final String TEMP_RESOURCE_PREFIX = "temp_"; private static final String WHO_CAN_USE_TEMP_FILE = "whoCanUse.tmp"; private static final String TEMP_RESOURCE_BY_URL_ADMIN_ONLY="TEMP_RESOURCE_BY_URL_ADMIN_ONLY"; - - /** - * Returns an empty TempFile of a unique id and file handle that can be used to write and access a - * temp file. The request will be used to create a fingerprint that will be written to the "allowList" and can - * be used to retreive the temp resource in other requests - * - * @param incomingFileName - * @param request - * @return - * @throws DotSecurityException + * Returns an empty TempFile of a unique id and file handle that can be used to write and access a temp file. The + * request will be used to create a fingerprint that will be written to the "allowList" and can be used to retrieve + * the temp resource in other requests. + * + * @param incomingFileName The name of the Temporary File. + * @param request The current instance of the {@link HttpServletRequest} + * + * @return The empty {@link DotTempFile}. + * + * @throws DotSecurityException An error occurred when creating the Temporary File. */ public DotTempFile createEmptyTempFile(final String incomingFileName,final HttpServletRequest request) throws DotSecurityException { final String anon = Try.of(() -> APILocator.getUserAPI().getAnonymousUser().getUserId()).getOrElse("anonymous"); @@ -126,77 +127,62 @@ public DotTempFile createEmptyTempFile(final String incomingFileName,final HttpS } /** - * This method takes a request and based upon it it returns the max file size that can be - * uploaded, based on the user uploading. Anonymous users can have smaller file size limitations - * than authenticated users. A return of -1 means unlimited. - * @param request - * @return + * This method takes a request and based upon it returns the max file size that can be uploaded, based on the user + * uploading. Anonymous users can have smaller file size limitations than authenticated users. A return of -1 means + * unlimited. + * + * @param request The current instance of the {@link HttpServletRequest} + * + * @return The maximum file size allowed by dotCMS. */ @VisibleForTesting public long maxFileSize(final HttpServletRequest request) { - - - final long requestedMax = ConversionUtils.toLongFromByteCountHumanDisplaySize(request.getParameter("maxFileLength"), -1); - final long systemMax = Config.getLongProperty(TEMP_RESOURCE_MAX_FILE_SIZE, -1l); - final long anonMax = Config.getLongProperty(TEMP_RESOURCE_MAX_FILE_SIZE_ANONYMOUS, -1l); + final long requestedMax = ConversionUtils.toLongFromByteCountHumanDisplaySize(request.getParameter(MAX_FILE_LENGTH_PARAM), -1L); + final long systemMax = Config.getLongProperty(TEMP_RESOURCE_MAX_FILE_SIZE, -1L); + final long anonMax = Config.getLongProperty(TEMP_RESOURCE_MAX_FILE_SIZE_ANONYMOUS, -1L); final boolean isAnon = PortalUtil.getUserId(request) == null || UserAPI.CMS_ANON_USER_ID.equals(PortalUtil.getUserId(request)); - List longs = (isAnon) ? Lists.newArrayList(requestedMax,systemMax,anonMax) : Lists.newArrayList(requestedMax,systemMax); + final List longs = (isAnon) ? Lists.newArrayList(requestedMax,systemMax,anonMax) : Lists.newArrayList(requestedMax,systemMax); longs.removeIf(i-> i < 0); Collections.sort(longs); - return longs.isEmpty() ? -1l : longs.get(0); - + return longs.isEmpty() ? -1L : longs.get(0); } - - - + /** - * Writes an InputStream to a temp file and returns the tempFile with a unique id and file handle - * that can be used to access the temp file. The request will be used to create a fingerprint - * that will be written to the "allowList" and can - * be used to retreive the temp resource in other requests - * - * @param incomingFileName - * @param request - * @param inputStream - * @return - * @throws DotSecurityException + * Writes an InputStream to a temp file and returns the tempFile with a unique id and file handle that can be used to + * access the temp file. The request will be used to create a fingerprint that will be written to the "allowList" and + * can be used to retrieve the temp resource in other requests. + * + * @param incomingFileName The name of the Temporary File. + * @param request The current instance of the {@link HttpServletRequest} + * @param inputStream The content of the Temporary File. + * + * @return The new {@link DotTempFile} + * + * @throws DotSecurityException An error occurred when creating the Temporary File. */ public DotTempFile createTempFile(final String incomingFileName,final HttpServletRequest request, final InputStream inputStream) throws DotSecurityException { - final DotTempFile dotTempFile = this.createEmptyTempFile(incomingFileName, request); - final File tempFile = dotTempFile.file; - final long maxLength = maxFileSize(request); - - try (final OutputStream out = new BoundedOutputStream(maxLength,Files.newOutputStream(tempFile.toPath()))) { - - - int read = 0; - final byte[] bytes = new byte[4096]; - while ((read = inputStream.read(bytes)) != -1) { - out.write(bytes, 0, read); - } - return dotTempFile; - } catch (IOException e) { - final String message = APILocator.getLanguageAPI().getStringKey(WebAPILocator.getLanguageWebAPI().getLanguage(request), "temp.file.max.file.size.error").replace("{0}", UtilMethods.prettyByteify(maxLength)); - throw new DotStateException(message, e); - } catch (Exception e) { - throw new DotRuntimeException(e.getMessage(), e); - } finally { - CloseUtils.closeQuietly(inputStream); - } + this.writeFile(request, dotTempFile, inputStream); + return dotTempFile; } /** - * Takes a url, downloads it and the returns the resulting file as tempFile with a unique id and - * file handle that can be used to access the temp file. The request will be used to create a fingerprint - * that will be written to the "allowList" and can be used to retreive the temp resource in other requests - * - * @param incomingFileName - * @param request - * @return - * @throws DotSecurityException + * Takes a URL, downloads it and the returns the resulting file as tempFile with a unique id and file handle that can + * be used to access the temp file. The request will be used to create a fingerprint that will be written to the + * "allowList" and can be used to retrieve the temp resource in other requests + * + * @param incomingFileName The name of the Temporary File, if required. + * @param request The current instance of the {@link HttpServletRequest} + * @param url The {@link URL} pointing to the file that must be retrieved. + * @param timeoutSeconds The specified timeout for reading the files via the URL. + * @param maxLength The maximum allowed size of the file being retrieved. + * + * @return The {@link DotTempFile} with the file specified via the URL. + * + * @throws DotSecurityException An error occurred when creating the Temporary File. + * @throws IOException An error occurred retrieving the contents of the file via URL. */ public DotTempFile createTempFileFromUrl(final String incomingFileName, final HttpServletRequest request, final URL url, final int timeoutSeconds, @@ -253,17 +239,44 @@ public DotTempFile createTempFileFromUrl(final String incomingFileName, } /** - * This method receives a URL and checks if starts with http or https, - * and also makes a request to the URL and if returns 200 the URL is valid, - * if returns any other response will be false - * @param url + * Updates the contents of an existing Temporary File. If the ID of such a file equals the word {@code "new"} or if + * the ID doesn't exist anymore, a new temporary file with the specified content will be returned instead. + * + * @param request The current instance of the {@link HttpServletRequest}. + * @param tempFileId The ID of the Temporary File. + * @param incomingFileName The actual file name of the Temporary File. This is used only when the original + * Temporary File ID doesn't exist. + * @param inputStream The new content of the Temporary File. + * + * @return The {@link DotTempFile} representing the specified file, or a new one if the ID doesn't exist. + * + * @throws DotSecurityException An error occurred when creating Temporary File. + */ + public DotTempFile upsertTempFile(final HttpServletRequest request, final String tempFileId, final String incomingFileName, final InputStream inputStream) throws DotSecurityException { + if ("new".equalsIgnoreCase(tempFileId)) { + return this.createTempFile(incomingFileName, request, inputStream); + } + final Optional dotTempFile = this.getTempFile(tempFileId); + if (dotTempFile.isEmpty()) { + return this.createTempFile(incomingFileName, request, inputStream); + } + this.writeFile(request, dotTempFile.get(), inputStream); + return dotTempFile.get(); + } + + /** + * This method receives a URL and checks if starts with http or https, and also makes a request to the URL and if + * returns 200 the URL is valid, if returns any other response will be false. + * + * @param url The specified URL + * * @return boolean if the url is valid or not */ public boolean validUrl(final String url) { if(!(url.toLowerCase().startsWith("http://") || url.toLowerCase().startsWith("https://"))){ - Logger.error(this, "URL does not starts with http or https"); + Logger.error(this, String.format("URL [ %s ] does not start with http or https", url)); return false; } try { @@ -277,6 +290,15 @@ public boolean validUrl(final String url) { return true; } + /** + * Resolves the name of the Temporary File based on either the specified desired name, or by retrieving it from the + * URL. + * + * @param desiredName The specified desired file name. + * @param url The URL that contains the file name. + * + * @return The name of the Temporary File. + */ private String resolveFileName(final String desiredName, final URL url) { final String path=(url!=null)? url.getPath() : UUIDGenerator.shorty(); final String tryFileName = (desiredName!=null) @@ -286,10 +308,21 @@ private String resolveFileName(final String desiredName, final URL url) { : path; return FileUtil.sanitizeFileName(tryFileName); } - - - - + + /** + * Creates a {@code whoCanUse.tmp} file for every empty Temporary File. Such a file contains the following + * information: + *
    + *
  • The User ID who created the Temporary File.
  • + *
  • The Session ID.
  • + *
  • The request's fingerprint.
  • + *
+ * + * @param parentFolder The folder that this TMP file will be created in. + * @param incomingAccessingList The list of properties that will be included in the file. + * + * @return The temporary permissions file. + */ private File createTempPermissionFile(final File parentFolder, final List incomingAccessingList) { List accessingList = new ArrayList<>(incomingAccessingList); accessingList.removeIf(Objects::isNull); @@ -304,6 +337,15 @@ private File createTempPermissionFile(final File parentFolder, final List getTempFile(final String tempFileId) { if (tempFileId == null || !tempFileId.startsWith(TEMP_RESOURCE_PREFIX)) { @@ -322,12 +364,18 @@ private Optional getTempFile(final String tempFileId) { return Optional.of(new DotTempFile(tempFileId, tempFile)); } - - Logger.error(this,"Temp File does not exists or TTL of the file already expired"); + Logger.error(this, String.format("Temp File '%s' does not exist or its TTL already expired", tempFileId)); return Optional.empty(); - } + /** + * Determines whether the incoming Access List has "use" permissions over a given Temporary File. + * + * @param incomingAccessingList The Access List being checked. + * @param dotTempFile The {@link DotTempFile} object. + * + * @return If the incoming list matches the existing permission list, returne {@code true}. + */ private boolean canUseTempFile(final List incomingAccessingList, final DotTempFile dotTempFile) { final File tempFile = dotTempFile.file; final List accessingList = new ArrayList<>(incomingAccessingList); @@ -340,7 +388,7 @@ private boolean canUseTempFile(final List incomingAccessingList, final D : new File(tempFile.getParentFile(), WHO_CAN_USE_TEMP_FILE); try { - final List perms = (file.exists()) ? new ObjectMapper().readValue(file, List.class) : ImmutableList.of(); + final List perms = (file.exists()) ? new ObjectMapper().readValue(file, List.class) : List.of(); return !Collections.disjoint(perms, accessingList); } catch (IOException e) { throw new DotStateException(e.getMessage(), e); @@ -348,14 +396,19 @@ private boolean canUseTempFile(final List incomingAccessingList, final D } /** - * Optionally retreives the Temp Resource. The temp resource will only be returned if 1) the - * accessingList contains a value that is also contained whoCanUse.tmp file that was created when - * the temp resource was written and 2) that the file modification time on the temp resource is - * newer than the value configured by TEMP_RESOURCE_MAX_AGE_SECONDS, which defaults to 30m. - * - * @param accessingList - * @param tempFileId - * @return + * Optionally retrieves the Temp Resource. The temp resource will only be returned if: + *
    + *
  1. The {@code accessingList} contains a value that is also contained in the {@code whoCanUse.tmp} file that + * was created when the temp resource was written.
  2. + *
  3. And that the file modification time on the temp resource is newer than the value configured by + * {@link #TEMP_RESOURCE_MAX_AGE_SECONDS}, which defaults to 30m.
  4. + *
+ * + * @param accessingList The incoming Access List. + * @param tempFileId The ID of the Temporary File. + * + * @return The optional with the {@link DotTempFile} if the Access List matched the existing one. Otherwise, an empty + * optional will be returned. */ public Optional getTempFile(final List accessingList, final String tempFileId) { Optional tempFile = getTempFile(tempFileId); @@ -366,15 +419,18 @@ public Optional getTempFile(final List accessingList, final } /** - * Optionally retreives the Temp Resource using the request. The temp - * resource will only be returned if 1) the fingerprint or sessionId is contained in the whoCanUse.tmp - * file that was created when the temp resource was written and 2) that the file modification time - * on the temp resource is newer than the value configured by TEMP_RESOURCE_MAX_AGE_SECONDS, which - * defaults to 30m. - * - * @param request - * @param tempFileId - * @return + * Optionally retrieves the Temp Resource using the request. The temp resource will only be returned if: + *
    + *
  1. The fingerprint or sessionId is contained in the {@code whoCanUse.tmp} file that was created when the temp + * resource was written.
  2. + *
  3. And that the file modification time on the temp resource is newer than the value configured by + * {@link #TEMP_RESOURCE_MAX_AGE_SECONDS}, which defaults to 30m.
  4. + *
+ * + * @param request The current instance of the {@link HttpServletRequest}. + * @param tempFileId The ID of the Temporary File. + * + * @return The optional with the {@link DotTempFile} */ public Optional getTempFile(final HttpServletRequest request, final String tempFileId) { final String anon = Try.of(() -> APILocator.getUserAPI().getAnonymousUser().getUserId()).getOrElse("anonymous"); @@ -397,10 +453,11 @@ public Optional getTempFile(final HttpServletRequest request, final } /** - * returns if a temp resource exits - * - * @param tempFileId - * @return + * Checks whether the specified Temporary File ID exists or not. + * + * @param tempFileId The ID of the Temporary File. + * + * @return If the Temporary File exists, returns {@code true}. */ public boolean isTempResource(final String tempFileId) { return getTempFile(tempFileId).isPresent(); @@ -416,10 +473,17 @@ public boolean accept(final File pathname) { ; } }; - + + /** + * Generates a String representing the fingerprint of the specified {@link HttpServletRequest}. It takes a specified + * set of properties from the request and generates a unique identifier for the request. + * + * @param request The current instance of the {@link HttpServletRequest}. + * + * @return The request's fingerprint. + */ public String getRequestFingerprint(final HttpServletRequest request) { - - final List uniqList = new ArrayList(); + final List uniqList = new ArrayList<>(); uniqList.add(request.getHeader("User-Agent")); uniqList.add(request.getHeader("Host")); uniqList.add(request.getHeader("Accept-Language")); @@ -440,10 +504,8 @@ public String getRequestFingerprint(final HttpServletRequest request) { } final String fingerPrint = String.join(" , ", uniqList); - Logger.info(this.getClass(), "Unique browser fingerprint: " + fingerPrint); + Logger.debug(this.getClass(), "Unique browser fingerprint: " + fingerPrint); return Encryptor.digest(fingerPrint); - - } /** @@ -464,4 +526,31 @@ public Optional getTempResourceId(final File file){ return Optional.empty(); } + /** + * Writes the specified content in the form of an Input Stream to the specified Temporary File. + * + * @param request The current instance of the {@link HttpServletRequest}. + * @param dotTempFile The {@link DotTempFile} whose content will be overwritten. + * @param inputStream The new file content as an {@link InputStream}. + */ + private void writeFile(final HttpServletRequest request, final DotTempFile dotTempFile, final InputStream inputStream) { + final File tempFile = dotTempFile.file; + final long maxLength = this.maxFileSize(request); + try (final OutputStream out = new BoundedOutputStream(maxLength, Files.newOutputStream(tempFile.toPath()))) { + int read; + final byte[] bytes = new byte[4096]; + while ((read = inputStream.read(bytes)) != -1) { + out.write(bytes, 0, read); + } + } catch (final IOException e) { + final String message = + APILocator.getLanguageAPI().getStringKey(WebAPILocator.getLanguageWebAPI().getLanguage(request), "temp.file.max.file.size.error").replace("{0}", UtilMethods.prettyByteify(maxLength)); + throw new DotStateException(message, e); + } catch (final Exception e) { + throw new DotRuntimeException(e.getMessage(), e); + } finally { + CloseUtils.closeQuietly(inputStream); + } + } + } diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/temp/TempFileResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/temp/TempFileResource.java index 1bd8fd7ab449..22793d24bec5 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/temp/TempFileResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/temp/TempFileResource.java @@ -2,37 +2,27 @@ import com.dotcms.concurrent.DotConcurrentFactory; import com.dotcms.concurrent.DotSubmitter; -import com.dotcms.mock.request.DotCMSMockRequest; -import com.dotcms.mock.request.DotCMSMockRequestWithSession; import com.dotcms.rest.AnonymousAccess; import com.dotcms.rest.ErrorEntity; -import com.dotcms.rest.InitDataObject; import com.dotcms.rest.WebResource; import com.dotcms.rest.annotation.NoCache; import com.dotcms.rest.api.v1.DotObjectMapperProvider; import com.dotcms.rest.api.v1.authentication.RequestUtil; import com.dotcms.rest.api.v1.authentication.ResponseUtil; -import com.dotcms.rest.api.v1.workflow.WorkflowResource; import com.dotcms.rest.exception.BadRequestException; -import com.dotcms.util.CollectionsUtils; import com.dotcms.util.SecurityUtils; -import com.dotcms.workflow.form.FireMultipleActionForm; -import com.dotmarketing.beans.Request; import com.dotmarketing.business.APILocator; import com.dotmarketing.exception.DoesNotExistException; -import com.dotmarketing.portlets.workflows.business.WorkflowAPI; import com.dotmarketing.util.Config; +import com.dotmarketing.util.FileUtil; import com.dotmarketing.util.Logger; -import com.dotmarketing.util.PageMode; import com.dotmarketing.util.UtilMethods; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; -import com.liferay.portal.util.WebKeys; import com.liferay.util.HttpHeaders; import com.liferay.util.StringPool; import io.vavr.control.Try; -import org.apache.commons.lang.time.StopWatch; import org.glassfish.jersey.media.multipart.BodyPart; import org.glassfish.jersey.media.multipart.ContentDisposition; import org.glassfish.jersey.media.multipart.FormDataMultiPart; @@ -40,11 +30,20 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import javax.ws.rs.*; +import javax.ws.rs.Consumes; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.StreamingOutput; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -52,16 +51,23 @@ import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; -import java.util.Map; +import java.util.Optional; import java.util.concurrent.CompletionService; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorCompletionService; import java.util.concurrent.Future; +/** + * This REST Endpoint allows Users to interact with the Temp File API in dotCMS. Temporary Files will live in the system + * for a specified amount of time -- 30 minutes by default. After that they won't be accessible anymore.. + * + * @author Will Ezell + * @since Jul 8th, 2019 + */ @Path("/v1/temp") public class TempFileResource { - public final static String MAX_FILE_LENGTH_PARAM ="maxFileLength"; + public static final String MAX_FILE_LENGTH_PARAM ="maxFileLength"; private final TempFileAPI tempApi; /** @@ -77,6 +83,17 @@ public TempFileResource() { this.tempApi = tempApi; } + /** + * Uploads a binary file to dotCMS via the Temp File API. + * + * @param request The current instance of the {@link HttpServletRequest}. + * @param response The current instance of the {@link HttpServletResponse}. + * @param maxFileLengthString The maximum allowed size for the uploaded file. If not specified, the dotCMS default + * value will be used instead. + * @param body The {@link FormDataMultiPart} object containing the file. + * + * @return A JSON response including important information related to the recently uploaded Temporary File. + */ @POST @JSONP @NoCache @@ -86,26 +103,14 @@ public final Response uploadTempResourceMulti(@Context final HttpServletRequest @Context final HttpServletResponse response, @DefaultValue("-1") @QueryParam(MAX_FILE_LENGTH_PARAM) final String maxFileLengthString, // this is being used later final FormDataMultiPart body) { - - verifyTempResourceEnabled(); - - final boolean allowAnonToUseTempFiles = Config - .getBooleanProperty(TempFileAPI.TEMP_RESOURCE_ALLOW_ANONYMOUS, true); - - new WebResource.InitBuilder(request, response) - .requiredAnonAccess(AnonymousAccess.WRITE) - .rejectWhenNoUser(!allowAnonToUseTempFiles) - .init(); - - if (!new SecurityUtils().validateReferer(request)) { - - throw new BadRequestException("Invalid Origin or referer"); - } - + this.checkEndpointAccess(request, response); return Response.ok(new MultipleBinaryStreamingOutput(body, request)) .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON).build(); } + /** + * Streaming Output class used for saving one or more binary files as Temporary Files. + */ protected class MultipleBinaryStreamingOutput implements StreamingOutput { private final FormDataMultiPart body; @@ -113,115 +118,210 @@ protected class MultipleBinaryStreamingOutput implements StreamingOutput { private MultipleBinaryStreamingOutput(final FormDataMultiPart body, final HttpServletRequest request) { - this.body = body; this.request = request; } @Override public void write(final OutputStream output) throws IOException, WebApplicationException { - final ObjectMapper objectMapper = DotObjectMapperProvider.getInstance().getDefaultObjectMapper(); - TempFileResource.this.saveMultipleBinary(body, request, output, objectMapper); + saveMultipleBinaryFiles(body, request, output, objectMapper); } - } - - private void saveMultipleBinary(final FormDataMultiPart body, final HttpServletRequest request, - final OutputStream outputStream, final ObjectMapper objectMapper) { - final DotSubmitter dotSubmitter = DotConcurrentFactory.getInstance().getSubmitter("TEMP_API_SUBMITTER", - new DotConcurrentFactory.SubmitterConfigBuilder().poolSize(2).maxPoolSize(5).queueCapacity(100000).build()); - final CompletionService completionService = new ExecutorCompletionService<>(dotSubmitter); - final List> futures = new ArrayList<>(); - final HttpServletRequest statelessRequest = RequestUtil.INSTANCE.createStatelessRequest(request); - - int index = 1; - for (final BodyPart part : body.getBodyParts()) { - - // this triggers the save - final int futureIndex = index; - final Future future = completionService.submit(() -> { - - try { - final InputStream in = (part.getEntity() instanceof InputStream) ? - InputStream.class.cast(part.getEntity()) - : Try.of(() -> part.getEntityAs(InputStream.class)).getOrNull(); - - if (in == null) { - - return new ErrorEntity(String.valueOf(HttpServletResponse.SC_BAD_REQUEST), "Invalid Binary Part, index: " + futureIndex, + /** + * Saves the binary file or files that are being submitted by the user. A Completion Service is used to improve the + * time it takes to save every Temporary File. + * + * @param body The {@link FormDataMultiPart} containing the file or files that will be saved. + * @param request The current instance of the {@link HttpServletRequest}. + * @param outputStream The streaming output of the response. + * @param objectMapper The {@link ObjectMapper} that transforms the response into a JSON object. + */ + private void saveMultipleBinaryFiles(final FormDataMultiPart body, final HttpServletRequest request, + final OutputStream outputStream, final ObjectMapper objectMapper) { + final CompletionService completionService = createCompletionService(2, 5, 100000); + final List> futures = new ArrayList<>(); + final HttpServletRequest statelessRequest = RequestUtil.INSTANCE.createStatelessRequest(request); + int index = 1; + for (final BodyPart part : body.getBodyParts()) { + // this triggers the save + final int futureIndex = index; + final Future future = completionService.submit(() -> { + + try { + final InputStream in = (part.getEntity() instanceof InputStream) ? (InputStream) part.getEntity() : + Try.of(() -> part.getEntityAs(InputStream.class)).getOrNull(); + final ContentDisposition meta = part.getContentDisposition(); + final Optional errorEntity = validateFileData(in, meta, futureIndex); + return errorEntity.isPresent() ? errorEntity.get() : + tempApi.createTempFile(meta.getFileName(), statelessRequest, in); + } catch (final Exception e) { + final String errorMsg = + "Invalid Binary Part, Message: " + e.getMessage() + ", index: " + futureIndex; + Logger.error(this, errorMsg, e); + return new ErrorEntity(String.valueOf(HttpServletResponse.SC_BAD_REQUEST), errorMsg, String.valueOf(futureIndex)); } - final ContentDisposition meta = part.getContentDisposition(); - if (meta == null) { + }); + ++index; + futures.add(future); + } + printResponseEntityViewResult(outputStream, objectMapper, completionService, futures); + } - return new ErrorEntity(String.valueOf(HttpServletResponse.SC_BAD_REQUEST), "Invalid Binary Part, index: " + futureIndex, - String.valueOf(futureIndex)); - } + /** + * Verifies that the information retrieved for the uploaded binary file is correct and readable. + * + * @param inputStream The content of the binary file as an {@link InputStream} object. + * @param meta The {@link ContentDisposition} containing the file's metadata. + * @param futureIndex The index representing the order in which this file is being processed. + * + * @return An {@link Optional} with the result of the validation. An empty optional means that no errors were + * found. + */ + private Optional validateFileData(final InputStream inputStream, final ContentDisposition meta, + final int futureIndex) { + if (null == inputStream) { + return Optional.of(new ErrorEntity(String.valueOf(HttpServletResponse.SC_BAD_REQUEST), "Invalid inout" + + " stream Binary Part, index: " + futureIndex, String.valueOf(futureIndex))); + } + if (null == meta) { + return Optional.of(new ErrorEntity(String.valueOf(HttpServletResponse.SC_BAD_REQUEST), "Invalid metadata Binary Part, index: " + futureIndex, String.valueOf(futureIndex))); + } + final String fileName = meta.getFileName(); + if (UtilMethods.isNotSet(fileName) || fileName.startsWith(StringPool.PERIOD) || fileName.contains("/.")) { + return Optional.of(new ErrorEntity(String.valueOf(HttpServletResponse.SC_BAD_REQUEST), "Invalid Binary Part, Name: " + fileName + ", index: " + futureIndex, String.valueOf(futureIndex))); + } + return Optional.empty(); + } - final String fileName = meta.getFileName(); - if (fileName == null || fileName.startsWith(".") || fileName.contains("/.")) { + } - return new ErrorEntity(String.valueOf(HttpServletResponse.SC_BAD_REQUEST), - "Invalid Binary Part, Name: " + fileName + ", index: " + futureIndex, - String.valueOf(futureIndex)); - } + /** + * Updates a specific Temporary File with the provided content as a String. If such a file doesn't exist or has + * expired, a brand new Temporary File with the specified content will be generated instead. + * + * @param request The current instance of the {@link HttpServletRequest}. + * @param response The current instance of the {@link HttpServletResponse}. + * @param tempFileId The ID of the Temporary File that will be updated. + * @param form The {@link PlainTextFileForm} with the required file information. + * + * @return A JSON response including important information related to the recently updated Temporary File. + */ + @PUT + @Path("/id/{tempFileId: .*}") + @JSONP + @NoCache + @Produces("application/octet-stream") + @Consumes(MediaType.APPLICATION_JSON) + public final Response upsertTempResource(@Context final HttpServletRequest request, + @Context final HttpServletResponse response, + @PathParam("tempFileId") final String tempFileId, + final PlainTextFileForm form) { + this.checkEndpointAccess(request, response, false); + final StreamingOutput streamingOutput = output -> { - return this.tempApi.createTempFile(fileName, statelessRequest, in); - } catch (Exception e) { + final ObjectMapper objectMapper = DotObjectMapperProvider.getInstance().getDefaultObjectMapper(); + TempFileResource.this.savePlainTextFile(request, objectMapper, output, tempFileId, form); - Logger.error(this, e.getMessage(), e); - return new ErrorEntity(String.valueOf(HttpServletResponse.SC_BAD_REQUEST), - "Invalid Binary Part, Message: " + e.getMessage() + ", index: " + futureIndex, - String.valueOf(futureIndex)); - } - }); + }; + return Response.ok(streamingOutput).header(HttpHeaders.CONTENT_TYPE, + MediaType.APPLICATION_JSON).build(); + } - ++index; - futures.add(future); - } + /** + * Overwrites a specific Temporary File with new content. + * + * @param request The current instance of the {@link HttpServletRequest}. + * @param objectMapper The {@link ObjectMapper} that transforms the response into a JSON object. + * @param outputStream The streaming output for the response. + * @param tempFileId The ID of the Temporary File that is being overwritten. + * @param form The {@link PlainTextFileForm} object with the information of the submitted file. + */ + private void savePlainTextFile(final HttpServletRequest request, final ObjectMapper objectMapper, final OutputStream outputStream, final String tempFileId, final PlainTextFileForm form) { + final CompletionService completionService = this.createCompletionService(1, 1, 10); + final Future future = this.updateTempFile(completionService, + RequestUtil.INSTANCE.createStatelessRequest(request), tempFileId, form.fileName(), + new ByteArrayInputStream(form.fileContent().getBytes())); + this.printResponseEntityViewResult(outputStream, objectMapper, completionService, List.of(future)); + } - printResponseEntityViewResult(outputStream, objectMapper, completionService, futures); + /** + * Saves the content of the specified Temporary File. In order to improve the performance, a + * {@link CompletionService} task care of creating a Future task that calls the Temp File API that actually saves + * the new content of the Temporary File. + * + * @param completionService The {@link CompletionService} instance that will update the Temporary File. + * @param statelessRequest A stateless {@link HttpServletRequest} taht is used to call the Temp File API. + * @param tempFileId The ID of the Temporary File that is being overwritten. + * @param fileName The name of the Temporary File. If the file doesn't exist or has expired, this value + * will be used to create the new Temporary File. + * @param in The new content of the file as an {@link InputStream} object. + * + * @return The {@link Future} task that will save the Temporary File. + */ + private Future updateTempFile(final CompletionService completionService, + final HttpServletRequest statelessRequest, + final String tempFileId, final String fileName, final InputStream in) { + return completionService.submit(() -> { + + try { + if (in == null) { + return new ErrorEntity(String.valueOf(HttpServletResponse.SC_BAD_REQUEST), "Invalid Binary Stream", fileName); + } + final String sanitizedFileName = FileUtil.sanitizeFileName(fileName); + return this.tempApi.upsertTempFile(statelessRequest, tempFileId, sanitizedFileName, in); + } catch (final Exception e) { + Logger.error(this, e.getMessage(), e); + return new ErrorEntity(String.valueOf(HttpServletResponse.SC_BAD_REQUEST), "Invalid Binary Part, " + + "Message: " + e.getMessage(), fileName); + } + + }); } + /** + * Returns the basic information of the created Temporary File as a JSON object. The data provided here is very + * useful for the service or user that called this endpoint in order to get a summary of the Temporary File that was + * created. + * + * @param outputStream The streaming output for the response. + * @param objectMapper The {@link ObjectMapper} that transforms the response into a JSON object. + * @param completionService The {@link CompletionService} instance containing the task that saved/updated the + * Temporary File. + * @param futures The list of {@link Future} tasks that were created when saving one or more files. + */ private void printResponseEntityViewResult(final OutputStream outputStream, final ObjectMapper objectMapper, final CompletionService completionService, final List> futures) { - try { - outputStream.write(StringPool.OPEN_CURLY_BRACE.getBytes(StandardCharsets.UTF_8)); ResponseUtil.beginWrapProperty(outputStream, "tempFiles", false); outputStream.write(StringPool.OPEN_BRACKET.getBytes(StandardCharsets.UTF_8)); // now recover the N results for (int i = 0; i < futures.size(); i++) { - try { - - Logger.info(this, "Recovering the result " + (i + 1) + " of " + futures.size()); + Logger.debug(this, "Recovering result " + (i + 1) + " of " + futures.size()); objectMapper.writeValue(outputStream, completionService.take().get()); - if (i < futures.size()-1) { outputStream.write(StringPool.COMMA.getBytes(StandardCharsets.UTF_8)); } - } catch (InterruptedException | ExecutionException | IOException e) { - + } catch (final InterruptedException e) { + Logger.error(this, "Thread has been interrupted: " + e.getMessage(), e); + Thread.currentThread().interrupt(); + } catch (final ExecutionException | IOException e) { Logger.error(this, e.getMessage(), e); } } - outputStream.write(StringPool.CLOSE_BRACKET.getBytes(StandardCharsets.UTF_8)); ResponseUtil.endWrapProperty(outputStream); - } catch (IOException e) { - + } catch (final IOException e) { Logger.error(this, e.getMessage(), e); } } - - @POST @Path("/byUrl") @JSONP @@ -230,22 +330,8 @@ private void printResponseEntityViewResult(final OutputStream outputStream, @Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_JSON}) public final Response copyTempFromUrl(@Context final HttpServletRequest request,@Context final HttpServletResponse response, final RemoteUrlForm form) { - try { - - verifyTempResourceEnabled(); - - final boolean allowAnonToUseTempFiles = Config - .getBooleanProperty(TempFileAPI.TEMP_RESOURCE_ALLOW_ANONYMOUS, true); - - new WebResource.InitBuilder(request, response) - .requiredAnonAccess(AnonymousAccess.WRITE) - .rejectWhenNoUser(!allowAnonToUseTempFiles) - .init(); - - if (!new SecurityUtils().validateReferer(request)) { - throw new BadRequestException("Invalid Origin or referer"); - } + this.checkEndpointAccess(request, response); if(!UtilMethods.isSet(form.remoteUrl)){ throw new BadRequestException("No Url passed"); } @@ -253,25 +339,79 @@ public final Response copyTempFromUrl(@Context final HttpServletRequest request, throw new BadRequestException("Invalid url attempted for tempFile : " + form.remoteUrl); } - final List tempFiles = new ArrayList(); + final List tempFiles = new ArrayList<>(); tempFiles.add(tempApi .createTempFileFromUrl(form.fileName, request, new URL(form.remoteUrl), form.urlTimeoutSeconds, form.maxFileLength)); return Response.ok(ImmutableMap.of("tempFiles", tempFiles)).build(); - - } catch (Exception e) { + } catch (final Exception e) { Logger.warnAndDebug(this.getClass(), e); return ResponseUtil.mapExceptionResponse(e); } } - private void verifyTempResourceEnabled(){ + /** + * Utility method that checks that this REST Endpoint can be safely accessed under given circumstances. For + * instance: + *
    + *
  • The Temp File Resources is enabled.
  • + *
  • Whether Anonymous Users can submit Temporary Files or not.
  • + *
  • The origin or referer must be valid for the current HTTP Request.
  • + *
+ * + * @param request The current instance of the {@link HttpServletRequest}. + * @param response The current instance of the {@link HttpServletResponse}. + */ + private void checkEndpointAccess(final HttpServletRequest request, final HttpServletResponse response) { + this.checkEndpointAccess(request, response, true); + } + + /** + * Utility method that checks that this REST Endpoint can be safely accessed under given circumstances. For + * instance: + *
    + *
  • The Temp File Resources is enabled.
  • + *
  • The origin or referer must be valid for the current HTTP Request.
  • + *
+ * You can explicitly restrict Temp File API access for Anonymous Users as well. + * + * @param request The current instance of the {@link HttpServletRequest}. + * @param response The current instance of the {@link HttpServletResponse}. + * @param allowAnonymousAccess If Anonymous Users are NOT supposed to access a method in this REST Endpoint, set + * this to {@code false}. Otherwise, this method will access the current dotCMS + * configuration to determine if Anonymous Users are able to call a given endpoint + * action or not -- see {@link TempFileAPI#TEMP_RESOURCE_ALLOW_ANONYMOUS}. + */ + private void checkEndpointAccess(final HttpServletRequest request, final HttpServletResponse response, + final boolean allowAnonymousAccess) { if (!Config.getBooleanProperty(TempFileAPI.TEMP_RESOURCE_ENABLED, true)) { - final String message = "Temp Files Resource is not enabled, please change the TEMP_RESOURCE_ENABLED to true in your properties file"; + final String message = "Temp Files Resource is not enabled, please change the TEMP_RESOURCE_ENABLED to " + + "true in your properties file"; Logger.error(this, message); throw new DoesNotExistException(message); } + final boolean allowAnonToUseTempFiles = + allowAnonymousAccess && Config.getBooleanProperty(TempFileAPI.TEMP_RESOURCE_ALLOW_ANONYMOUS, true); + new WebResource.InitBuilder(request, response).requiredAnonAccess(AnonymousAccess.WRITE).rejectWhenNoUser(!allowAnonToUseTempFiles).init(); + if (!new SecurityUtils().validateReferer(request)) { + throw new BadRequestException("Invalid Origin or referer"); + } + } + + /** + * Creates a Completion Service with the specified configuration parameters. + * + * @param poolSize The initial size of the thread pool. + * @param maxPoolSize The maximum number of threads in the pool. + * @param queueCapacity The maximum capacity of the queue that will be processed. + * + * @return The {@link CompletionService} instance. + */ + private CompletionService createCompletionService(final int poolSize, final int maxPoolSize, final int queueCapacity) { + final DotSubmitter dotSubmitter = DotConcurrentFactory.getInstance().getSubmitter("TEMP_API_SUBMITTER", + new DotConcurrentFactory.SubmitterConfigBuilder().poolSize(poolSize).maxPoolSize(maxPoolSize).queueCapacity(queueCapacity).build()); + return new ExecutorCompletionService<>(dotSubmitter); } } diff --git a/dotCMS/src/main/webapp/html/portlet/ext/contentlet/field/edit_field.jsp b/dotCMS/src/main/webapp/html/portlet/ext/contentlet/field/edit_field.jsp index 932e22691715..7e63e66457cf 100644 --- a/dotCMS/src/main/webapp/html/portlet/ext/contentlet/field/edit_field.jsp +++ b/dotCMS/src/main/webapp/html/portlet/ext/contentlet/field/edit_field.jsp @@ -810,26 +810,13 @@ - - <% - - if(UtilMethods.isSet(value) && UtilMethods.isSet(resourceLink)){ - - boolean canUserWriteToContentlet = APILocator.getPermissionAPI().doesUserHavePermission(contentlet,PermissionAPI.PERMISSION_WRITE, user); - - %> - - <%if(canUserWriteToContentlet){%> - <% if (resourceLink.isEditableAsText()) { %> - <% - if (InodeUtils.isSet(binInode) && canUserWriteToContentlet) { - - %> - <%@ include file="/html/portlet/ext/contentlet/field/edit_file_asset_text_inc.jsp"%> - <% } %> - <% } %> - - <% } %> + <% if (UtilMethods.isSet(value)) { + final boolean canUserWriteToContentlet = APILocator.getPermissionAPI().doesUserHavePermission(contentlet, PermissionAPI.PERMISSION_WRITE, user); + if (canUserWriteToContentlet && resourceLink.isEditableAsText() && InodeUtils.isSet(binInode)) { %> + <%@ include file="/html/portlet/ext/contentlet/field/edit_file_asset_text_inc.jsp"%> + <% } %> + <% } else { %> + <%@ include file="/html/portlet/ext/contentlet/field/edit_file_asset_text_inc.jsp"%> <% } %> diff --git a/dotCMS/src/main/webapp/html/portlet/ext/contentlet/field/edit_file_asset_text_inc.jsp b/dotCMS/src/main/webapp/html/portlet/ext/contentlet/field/edit_file_asset_text_inc.jsp index 8cc52f635c65..34c9026d69b9 100644 --- a/dotCMS/src/main/webapp/html/portlet/ext/contentlet/field/edit_file_asset_text_inc.jsp +++ b/dotCMS/src/main/webapp/html/portlet/ext/contentlet/field/edit_file_asset_text_inc.jsp @@ -1,5 +1,16 @@ -<%String contents =UtilMethods.htmlifyString(FileUtils.readFileToString(contentlet.getBinary(field.getVelocityVarName()))); %> +<%@ page import="java.io.File" %> +<%@ page import="com.dotmarketing.portlets.contentlet.model.Contentlet" %> +<%@ page import="org.apache.commons.io.FileUtils" %> +<% + final File file = contentlet.getBinary(field.getVelocityVarName()); + String contents = ""; + String fileExtension = "txt"; + if (null != file) { + contents = UtilMethods.htmlifyString(FileUtils.readFileToString(file)); + fileExtension = UtilMethods.getFileExtension(file.getName()); + } +%>