diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/CreateLinksController.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/CreateLinksController.java index 8d0ea523443a3bfb9345279eb18581b4226c6fd1..b3dfe7fc7e1e36bfe45dbbde519cc426c8933c6b 100644 --- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/CreateLinksController.java +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/CreateLinksController.java @@ -10,6 +10,8 @@ import it.inaf.ia2.vospace.ui.data.CreateLinkRequest; import it.inaf.ia2.vospace.ui.exception.BadRequestException; import it.inaf.ia2.vospace.ui.exception.VOSpaceStatusException; import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -18,6 +20,7 @@ import net.ivoa.xml.vospace.v2.LinkNode; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; @@ -31,6 +34,9 @@ public class CreateLinksController extends BaseController { private static final Logger LOG = LoggerFactory.getLogger(CreateLinksController.class); + @Value("${list-of-links.limit:1000}") + private int listOfLinksSizeLimit; + @Autowired private VOSpaceClient client; @@ -63,11 +69,17 @@ public class CreateLinksController extends BaseController { List<CompletableFuture<?>> currentHttpCallsGroup = new ArrayList<>(); httpCallsGroups.add(currentHttpCallsGroup); - for (String url : fileContent.replaceAll("\\r\\n?", "\n").split("\n")) { // normalize newlines and split on them + // normalize newlines and split on them + String[] urls = fileContent.replaceAll("\\r\\n?", "\n").split("\n"); + if (urls.length > listOfLinksSizeLimit) { + throw new BadRequestException("List is too large: " + urls.length + " lines detected, limit is " + listOfLinksSizeLimit); + } + + for (String url : urls) { if (!url.isBlank()) { - String fileName = url.substring(url.lastIndexOf("/") + 1); - String uri = parent.getUri() + "/" + fileName; + url = url.trim(); + String uri = parent.getUri() + "/" + getFileNameFromUrl(url); LinkNode link = new LinkNode(); link.setUri(uri); @@ -89,6 +101,28 @@ public class CreateLinksController extends BaseController { return ResponseEntity.noContent().build(); } + private String getFileNameFromUrl(String url) { + + try { + // parse URL and remove the query string + String urlPath = new URL(url).getPath(); + if (urlPath.endsWith("/")) { + // remove last char if it is a slash + urlPath = urlPath.substring(0, urlPath.length() - 1); + } + if (urlPath.isEmpty() || !urlPath.contains("/")) { + throw new BadRequestException("Unable to extract file name from URL " + url); + } + String fileName = urlPath.substring(urlPath.lastIndexOf("/") + 1); + if (fileName.isEmpty()) { + throw new BadRequestException("Unable to extract file name from URL " + url); + } + return fileName; + } catch (MalformedURLException ex) { + throw new BadRequestException("Invalid URL: " + url); + } + } + private ContainerNode getFolder(String folderPath) { try { return (ContainerNode) client.getNode("/" + folderPath); diff --git a/vospace-ui-backend/src/main/resources/application.properties b/vospace-ui-backend/src/main/resources/application.properties index e72cdd41809901cb05e5de710f61edef1e2ca0bb..dc3177cd4ef06e3a88f9a9176f93501754269baf 100644 --- a/vospace-ui-backend/src/main/resources/application.properties +++ b/vospace-ui-backend/src/main/resources/application.properties @@ -11,6 +11,7 @@ cors.allowed.origin=http://localhost:8080 logging.level.it.inaf=TRACE trusted.eppn.scope=inaf.it +list-of-links.limit=1000 support.contact.label=IA2 team support.contact.email=ia2@inaf.it diff --git a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/CreateLinksControllerTest.java b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/CreateLinksControllerTest.java index f1eb4d7a69e1ad5b676b528214fa6995aba7995c..92c56f37b89ce9095db770d5d8a58e844be22f56 100644 --- a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/CreateLinksControllerTest.java +++ b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/CreateLinksControllerTest.java @@ -10,6 +10,7 @@ import it.inaf.ia2.aa.data.User; import it.inaf.ia2.vospace.ui.client.VOSpaceClient; import it.inaf.ia2.vospace.ui.data.CreateLinkRequest; import it.inaf.ia2.vospace.ui.exception.VOSpaceStatusException; +import java.util.Collections; import net.ivoa.xml.vospace.v2.ContainerNode; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -109,6 +110,41 @@ public class CreateLinksControllerTest { verify(client, times(75)).createNode(any()); } + @Test + public void testInvalidUrl() throws Exception { + testInvalidContent("foo"); + } + + @Test + public void testInvalidUrlNoFile() throws Exception { + testInvalidContent("http://archives.ia2.inaf.it/"); + } + + @Test + public void testInvalidUrlNoFileNoSlash() throws Exception { + testInvalidContent("http://archives.ia2.inaf.it"); + } + + @Test + public void testTooManyLinks() throws Exception { + testInvalidContent(String.join("\n", Collections.nCopies(1500, "http://archives.ia2.inaf.it/files/aao/SC182172.fits.gz"))); + } + + private void testInvalidContent(String fileContent) throws Exception { + + ContainerNode myFolder = new ContainerNode(); + when(client.getNode("/path/to/myfolder")).thenReturn(myFolder); + + MockMultipartFile file = new MockMultipartFile("file", fileContent.getBytes()); + + mockMvc.perform(multipart("/uploadLinks") + .file(file) + .param("folder", "path/to/myfolder") + .sessionAttr("user_data", user)) + .andDo(print()) + .andExpect(status().isBadRequest()); + } + private MockMultipartFile getListOfLinksMockMultipartFile() throws Exception { return new MockMultipartFile("file", UploadControllerTest.class.getClassLoader().getResourceAsStream("list-of-links.txt")); } diff --git a/vospace-ui-backend/src/test/resources/list-of-links.txt b/vospace-ui-backend/src/test/resources/list-of-links.txt index 0474fcedd9722a356c2a0d229ba8c4e625aff7e3..b3d48ffa0597fb96ef2bddf86512e02a065784e3 100644 --- a/vospace-ui-backend/src/test/resources/list-of-links.txt +++ b/vospace-ui-backend/src/test/resources/list-of-links.txt @@ -1,7 +1,7 @@ http://archives.ia2.inaf.it/files/aao/SC182159.fits.gz -http://archives.ia2.inaf.it/files/aao/SC182160.fits.gz -http://archives.ia2.inaf.it/files/aao/SC182161.fits.gz -http://archives.ia2.inaf.it/files/aao/SC182169.fits.gz +http://archives.ia2.inaf.it/files/aao/SC182160.fits.gz/ +http://archives.ia2.inaf.it/files/aao/SC182161.fits.gz?query=xxx +http://archives.ia2.inaf.it/files/aao/SC182169.fits.gz http://archives.ia2.inaf.it/files/aao/SC182170.fits.gz http://archives.ia2.inaf.it/files/aao/SC182171.fits.gz http://archives.ia2.inaf.it/files/aao/SC182172.fits.gz diff --git a/vospace-ui-frontend/src/components/modal/CreateLinksModal.vue b/vospace-ui-frontend/src/components/modal/CreateLinksModal.vue index ec1f5409cffe17e33cb3a6fd0231b9291371ac0a..eee6cbc45f1afe1d1b56257bfd92d05e9730dbf7 100644 --- a/vospace-ui-frontend/src/components/modal/CreateLinksModal.vue +++ b/vospace-ui-frontend/src/components/modal/CreateLinksModal.vue @@ -24,7 +24,10 @@ <b-form-invalid-feedback id="node-name-input-feedback" class="text-right">{{nodeNameError}}</b-form-invalid-feedback> </b-form> <b-form inline v-if="mode === 'multiple'"> - <p>Upload list of links (separated by newlines)</p> + <p> + Upload list of links (separated by newlines)<br /> + <em>Maximum 1000 links per file!</em> + </p> <b-form-file class="text-left" v-model="file" :multiple="false" placeholder="Choose your file or drop it here..." drop-placeholder="Drop file here..." :state="fileState"></b-form-file> <b-form-invalid-feedback id="file-input-feedback" class="text-right">{{fileError}}</b-form-invalid-feedback> </b-form>