diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java index 36a4324a474ee8e8744726b5794e871ce7cc88aa..a67829da95982884b95059e10dfe27d8f7566642 100644 --- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/client/VOSpaceClient.java @@ -5,6 +5,7 @@ import it.inaf.ia2.aa.data.User; import it.inaf.ia2.vospace.ui.VOSpaceUiApplication; import it.inaf.ia2.vospace.ui.data.Job; import it.inaf.ia2.vospace.ui.exception.VOSpaceException; +import static it.inaf.oats.vospace.datamodel.NodeUtils.urlEncodePath; import java.io.IOException; import java.io.InputStream; import java.io.StringWriter; @@ -72,7 +73,7 @@ public class VOSpaceClient { public Node getNode(String path) { - HttpRequest request = getRequest("/nodes" + path) + HttpRequest request = getRequest("/nodes" + urlEncodePath(path)) .header("Accept", useJson ? "application/json" : "text/xml") .build(); @@ -105,7 +106,7 @@ public class VOSpaceClient { String path = node.getUri().substring(("vos://" + authority).length()); - HttpRequest request = getRequest("/nodes" + path) + HttpRequest request = getRequest("/nodes" + urlEncodePath(path)) .header("Accept", useJson ? "application/json" : "text/xml") .header("Content-Type", useJson ? "application/json" : "text/xml") .PUT(HttpRequest.BodyPublishers.ofString(marshal(node))) @@ -113,10 +114,10 @@ public class VOSpaceClient { return call(request, BodyHandlers.ofInputStream(), 200, res -> unmarshal(res, Node.class)); } - + public void deleteNode(String path) { - HttpRequest request = getRequest("/nodes" + path) + HttpRequest request = getRequest("/nodes" + urlEncodePath(path)) .header("Accept", useJson ? "application/json" : "text/xml") .header("Content-Type", useJson ? "application/json" : "text/xml") .DELETE() diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java index 4e12e12c2b6c5a03285241fb7a6de52fead5650f..a4fb6569ac65b849cf4d876bec4c997481970ebd 100644 --- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java @@ -4,12 +4,15 @@ import it.inaf.ia2.aa.data.User; import it.inaf.ia2.vospace.ui.client.VOSpaceClient; import it.inaf.ia2.vospace.ui.data.ListNodeData; import it.inaf.ia2.vospace.ui.service.NodesService; +import it.inaf.oats.vospace.datamodel.NodeUtils; import java.util.Map; import javax.servlet.http.HttpServletRequest; import net.ivoa.xml.vospace.v2.ContainerNode; import net.ivoa.xml.vospace.v2.Property; import net.ivoa.xml.vospace.v2.Protocol; import net.ivoa.xml.vospace.v2.Transfer; +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.HttpHeaders; @@ -25,6 +28,8 @@ import org.springframework.web.bind.annotation.RestController; @RestController public class NodesController extends BaseController { + private static final Logger LOG = LoggerFactory.getLogger(NodesController.class); + @Value("${vospace-authority}") private String authority; @@ -41,6 +46,7 @@ public class NodesController extends BaseController { public ResponseEntity<ListNodeData> listNodes(User principal) throws Exception { String path = getPath("/nodes/"); + LOG.debug("listNodes called for path {}", path); return ResponseEntity.ok(nodesService.generateNodesHtml(path, principal)); } @@ -48,13 +54,15 @@ public class NodesController extends BaseController { @DeleteMapping(value = {"/nodes", "/nodes/**"}) public void deleteNode() { String path = getPath("/nodes/"); + LOG.debug("deleteNode called for path {}", path); client.deleteNode(path); } - + @GetMapping(value = "/download/**") public ResponseEntity<?> directDownload() { String path = getPath("/download/"); + LOG.debug("directDownload called for path {}", path); Transfer transfer = new Transfer(); transfer.setDirection("pullFromVoSpace"); @@ -78,10 +86,12 @@ public class NodesController extends BaseController { parentPath = "/" + parentPath; } String name = getRequiredParam(params, "name"); + + LOG.debug("newFolder called for path {}/{}", parentPath, name); ContainerNode node = new ContainerNode(); node.setUri("vos://" + authority + parentPath + "/" + name); - + Property creator = new Property(); creator.setUri("ivo://ivoa.net/vospace/core#creator"); creator.setValue(getUser().getName()); @@ -90,20 +100,8 @@ public class NodesController extends BaseController { client.createNode(node); } - /** - * Slash is a special character in defining REST endpoints and trying to - * define a PathVariable containing slashes doesn't work, so the endpoint - * has been defined using "/nodes/**" instead of "/nodes/{path}" and the - * path is extracted manually parsing the request URL. - */ protected String getPath(String prefix) { String requestURL = servletRequest.getRequestURL().toString(); - String[] split = requestURL.split(prefix); - - String path = "/"; - if (split.length == 2) { - path += split[1]; - } - return path; + return NodeUtils.getPathFromRequestURLString(requestURL, prefix); } } diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesService.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesService.java index d8a5b4e0785f5eae5f12aa6a95a73ecf7c724c3e..f341a75883e3a15f6821e1a5313e00bb54be7d3e 100644 --- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesService.java +++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesService.java @@ -4,6 +4,7 @@ import it.inaf.ia2.aa.data.User; import it.inaf.ia2.vospace.ui.client.VOSpaceClient; import it.inaf.ia2.vospace.ui.data.ListNodeData; import it.inaf.oats.vospace.datamodel.NodeUtils; +import static it.inaf.oats.vospace.datamodel.NodeUtils.urlEncodePath; import java.io.IOException; import java.io.StringWriter; import java.io.UncheckedIOException; @@ -65,7 +66,7 @@ public class NodesService { } String html = "<tr>"; - html += "<td><input type=\"checkbox\" data-node=\"" + nodeInfo.getPath() + "\" "; + html += "<td><input type=\"checkbox\" data-node=\"" + nodeInfo.getPath().replace("\"", "\\\"") + "\" "; if (nodeInfo.isAsyncTrans()) { html += "class=\"async\""; } @@ -108,9 +109,9 @@ public class NodesService { private String getLink(NodeInfo nodeInfo, User user) { if (isDownloadable(nodeInfo, user)) { if (nodeInfo.isFolder()) { - return "<a href=\"#/nodes" + nodeInfo.getPath() + "\">" + nodeInfo.getName() + "</a>"; + return "<a href=\"#/nodes" + urlEncodePath(nodeInfo.getPath()) + "\">" + nodeInfo.getName() + "</a>"; } else { - return "<a href=\"download" + nodeInfo.getPath() + "\" target=\"blank_\">" + nodeInfo.getName() + "</a>"; + return "<a href=\"download" + urlEncodePath(nodeInfo.getPath()) + "\" target=\"blank_\">" + nodeInfo.getName() + "</a>"; } } return nodeInfo.getName(); diff --git a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/client/VOSpaceClientTest.java b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/client/VOSpaceClientTest.java index 5cd931c097606454513fc6f4962ee319299a051a..e8a1eabc6463733408e972734c2c2e19ab8f4e14 100644 --- a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/client/VOSpaceClientTest.java +++ b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/client/VOSpaceClientTest.java @@ -76,6 +76,20 @@ public class VOSpaceClientTest { assertEquals(newNode.getUri(), responseNode.getUri()); } + @Test + public void testCreateNodeBadName() { + + ContainerNode newNode = new ContainerNode(); + newNode.setUri("vos://ia2.inaf.it!vospace/mynode/File with spaces.and.dots.pdf"); + + ReflectionTestUtils.setField(voSpaceClient, "useJson", false); + + CompletableFuture response = getMockedStreamResponseFuture(200, getResourceFileContent("node-response.xml")); + when(mockedHttpClient.sendAsync(any(), any())).thenReturn(response); + + voSpaceClient.createNode(newNode); + } + protected static String getResourceFileContent(String fileName) { try ( InputStream in = VOSpaceClientTest.class.getClassLoader().getResourceAsStream(fileName)) { return new String(in.readAllBytes(), StandardCharsets.UTF_8); diff --git a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/NodesControllerTest.java b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/NodesControllerTest.java index 085b3f22f704aabb419365db1b8fbbfc89578261..79b0fada9c875c4b8b21bd05a3021fc594191c80 100644 --- a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/NodesControllerTest.java +++ b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/NodesControllerTest.java @@ -22,7 +22,7 @@ public class NodesControllerTest { @Autowired private MockMvc mockMvc; - + @Test public void testListNodesEmpty() throws Exception { diff --git a/vospace-ui-frontend/src/api/server/index.js b/vospace-ui-frontend/src/api/server/index.js index d7931218a42398532d792b5b379ed3947049328f..0cad25df45dfecb676b3c7638f9c41a9a545b6a4 100644 --- a/vospace-ui-frontend/src/api/server/index.js +++ b/vospace-ui-frontend/src/api/server/index.js @@ -41,9 +41,13 @@ function getErrorMessage(error) { } } +function escapePath(path) { + return path.split('/').map(p => encodeURIComponent(p)).join('/'); +} + export default { getNode(path) { - let url = BASE_API_URL + 'nodes/' + path; + let url = BASE_API_URL + 'nodes/' + escapePath(path); return apiRequest({ method: 'GET', url: url, @@ -127,7 +131,7 @@ export default { }) }, deleteNode(path) { - let url = BASE_API_URL + 'nodes' + path; + let url = BASE_API_URL + 'nodes' + escapePath(path); return apiRequest({ method: 'DELETE', url: url, diff --git a/vospace-ui-frontend/src/components/Main.vue b/vospace-ui-frontend/src/components/Main.vue index e9147d76018db4fb8247e89cb71cd3fd70033389..3038c24d201c005f6a020af920a68018a5e32276 100644 --- a/vospace-ui-frontend/src/components/Main.vue +++ b/vospace-ui-frontend/src/components/Main.vue @@ -60,7 +60,7 @@ export default { for (let i = 0; i < pathSplit.length; i++) { items.push({ text: pathSplit[i], - href: '#/nodes/' + pathSplit.slice(0, i + 1).join('/') + href: '#/nodes/' + pathSplit.slice(0, i + 1).map(p => encodeURIComponent(p)).join('/') }); } } diff --git a/vospace-ui-frontend/src/components/modal/CreateFolderModal.vue b/vospace-ui-frontend/src/components/modal/CreateFolderModal.vue index 41ef2dc7b24567ace7b84dec2ab7abb7420b41df..d671009b8210fe5ee68e78aa6e3ea967a3e08cc7 100644 --- a/vospace-ui-frontend/src/components/modal/CreateFolderModal.vue +++ b/vospace-ui-frontend/src/components/modal/CreateFolderModal.vue @@ -43,6 +43,8 @@ export default { if (!this.newFolderName) { this.newFolderNameError = "Folder name is required"; + } else if (/[<>?":\\/`|'*]/.test(this.newFolderName)) { + this.newFolderNameError = "Folder name contains an illegal character. Following characters are not allowed: < > ? \" : \\ / | ' * `"; } else { this.$store.dispatch('createFolder', this.newFolderName) .then(() => { diff --git a/vospace-ui-frontend/src/components/modal/UploadFilesModal.vue b/vospace-ui-frontend/src/components/modal/UploadFilesModal.vue index 90216f504c621ceced7b7b140e24b92dee52ca46..849c38b33367e93c68eb73080fd1fb7e41ea696e 100644 --- a/vospace-ui-frontend/src/components/modal/UploadFilesModal.vue +++ b/vospace-ui-frontend/src/components/modal/UploadFilesModal.vue @@ -1,6 +1,6 @@ <template> <b-modal id="upload-files-modal" title="Upload file" okTitle="Upload" @show="reset" @ok="uploadFiles"> - <b-form-file v-model="files" :multiple="true" :state="fileState" placeholder="Choose your files or drop them here..." drop-placeholder="Drop files here..."></b-form-file> + <b-form-file v-model="files" :multiple="true" :state="fileState" placeholder="Choose your files or drop them here..." drop-placeholder="Drop files here..." @change="resetError"></b-form-file> <b-form-invalid-feedback id="upload-file-input-feedback" class="text-right">{{uploadFileError}}</b-form-invalid-feedback> <div class="mt-3">Selected files: {{ selectedFiles }}</div> </b-modal> @@ -46,6 +46,14 @@ export default { if (this.files.length === 0) { this.uploadFileError = "Select at least one file"; } else { + // Check special characters in file names + for (let file of this.files) { + if (/[<>?":\\/`|'*]/.test(file.name)) { + this.uploadFileError = "File " + file.name + " contains an illegal character. Following characters are not allowed: < > ? \" : \\ / | ' * `"; + return; + } + } + this.$store.dispatch('uploadFiles', this.files) .then(() => { this.$bvModal.hide('upload-files-modal');