diff --git a/config/alfresco/public-rest-context.xml b/config/alfresco/public-rest-context.xml index 8265ddda86..3dfdafb932 100644 --- a/config/alfresco/public-rest-context.xml +++ b/config/alfresco/public-rest-context.xml @@ -1116,6 +1116,7 @@ + diff --git a/source/java/org/alfresco/rest/api/Nodes.java b/source/java/org/alfresco/rest/api/Nodes.java index f67b8ccb19..19a15b2cec 100644 --- a/source/java/org/alfresco/rest/api/Nodes.java +++ b/source/java/org/alfresco/rest/api/Nodes.java @@ -157,6 +157,16 @@ public interface Nodes */ BinaryResource getContent(String fileNodeId, Parameters parameters, boolean recordActivity); + /** + * Download file content. + * + * @param nodeRef the content nodeRef + * @param parameters + * @param recordActivity true, if an activity post is required. + * @return + */ + BinaryResource getContent(NodeRef nodeRef, Parameters parameters, boolean recordActivity); + /** * Uploads file content (updates existing node with new content). * @@ -215,7 +225,7 @@ public interface Nodes String PATH_SHARED = "-shared-"; String OP_CREATE = "create"; - String OP_DELETE= "delete"; + String OP_DELETE = "delete"; String OP_UPDATE = "update"; String PARAM_RELATIVE_PATH = "relativePath"; @@ -244,6 +254,4 @@ public interface Nodes String PARAM_VERSION_MAJOR = "majorVersion"; // true if major, false if minor String PARAM_VERSION_COMMENT = "comment"; - - String PARAM_RENDITIONS = "renditions"; } diff --git a/source/java/org/alfresco/rest/api/Renditions.java b/source/java/org/alfresco/rest/api/Renditions.java index 1f33e517fd..57f49d523b 100644 --- a/source/java/org/alfresco/rest/api/Renditions.java +++ b/source/java/org/alfresco/rest/api/Renditions.java @@ -23,6 +23,7 @@ import org.alfresco.rest.api.model.Rendition; import org.alfresco.rest.framework.resource.content.BinaryResource; import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; import org.alfresco.rest.framework.resource.parameters.Parameters; +import org.alfresco.service.cmr.repository.NodeRef; /** * Renditions API @@ -31,6 +32,8 @@ import org.alfresco.rest.framework.resource.parameters.Parameters; */ public interface Renditions { + String PARAM_STATUS = "status"; + /** * Lists all available renditions includes those that have been created and those that are yet to be created. * @@ -70,5 +73,13 @@ public interface Renditions */ BinaryResource getContent(String nodeId, String renditionId, Parameters parameters); - String PARAM_STATUS = "status"; + /** + * Downloads rendition. + * + * @param sourceNodeRef the source nodeRef + * @param renditionId the rendition id + * @param parameters the {@link Parameters} object to get the parameters passed into the request + * @return the rendition stream + */ + BinaryResource getContent(NodeRef sourceNodeRef, String renditionId, Parameters parameters); } diff --git a/source/java/org/alfresco/rest/api/impl/NodesImpl.java b/source/java/org/alfresco/rest/api/impl/NodesImpl.java index 15127ae42e..af8ef3018e 100644 --- a/source/java/org/alfresco/rest/api/impl/NodesImpl.java +++ b/source/java/org/alfresco/rest/api/impl/NodesImpl.java @@ -1950,15 +1950,20 @@ public class NodesImpl implements Nodes public BinaryResource getContent(String fileNodeId, Parameters parameters, boolean recordActivity) { final NodeRef nodeRef = validateNode(fileNodeId); + return getContent(nodeRef, parameters, recordActivity); + } - if (! nodeMatches(nodeRef, Collections.singleton(ContentModel.TYPE_CONTENT), null, false)) + @Override + public BinaryResource getContent(NodeRef nodeRef, Parameters parameters, boolean recordActivity) + { + if (!nodeMatches(nodeRef, Collections.singleton(ContentModel.TYPE_CONTENT), null, false)) { - throw new InvalidArgumentException("NodeId of content is expected: "+nodeRef.getId()); + throw new InvalidArgumentException("NodeId of content is expected: " + nodeRef.getId()); } Map nodeProps = nodeService.getProperties(nodeRef); - ContentData cd = (ContentData)nodeProps.get(ContentModel.PROP_CONTENT); - String name = (String)nodeProps.get(ContentModel.PROP_NAME); + ContentData cd = (ContentData) nodeProps.get(ContentModel.PROP_CONTENT); + String name = (String) nodeProps.get(ContentModel.PROP_NAME); org.alfresco.rest.framework.resource.content.ContentInfo ci = null; String mimeType = null; @@ -1982,7 +1987,7 @@ public class NodesImpl implements Nodes } else { - logger.warn("Ignored attachment=false for "+fileNodeId+" since "+mimeType+" is not in the whitelist for non-attach content types"); + logger.warn("Ignored attachment=false for "+nodeRef.getId()+" since "+mimeType+" is not in the whitelist for non-attach content types"); } } } @@ -1990,7 +1995,7 @@ public class NodesImpl implements Nodes if (recordActivity) { - final ActivityInfo activityInfo = getActivityInfo(getParentNodeRef(nodeRef), nodeRef); + final ActivityInfo activityInfo = getActivityInfo(getParentNodeRef(nodeRef), nodeRef); postActivity(Activity_Type.DOWNLOADED, activityInfo, true); } diff --git a/source/java/org/alfresco/rest/api/impl/QuickShareLinksImpl.java b/source/java/org/alfresco/rest/api/impl/QuickShareLinksImpl.java index 9b6e6949fa..a941029bde 100644 --- a/source/java/org/alfresco/rest/api/impl/QuickShareLinksImpl.java +++ b/source/java/org/alfresco/rest/api/impl/QuickShareLinksImpl.java @@ -230,15 +230,13 @@ public class QuickShareLinksImpl implements QuickShareLinks, InitializingBean throw new InvalidNodeRefException(nodeRef); } - String nodeId = nodeRef.getId(); - if (renditionId != null) { - return renditions.getContent(nodeId, renditionId, parameters); + return renditions.getContent(nodeRef, renditionId, parameters); } else { - return nodes.getContent(nodeId, parameters, false); + return nodes.getContent(nodeRef, parameters, false); } } }, networkTenantDomain); diff --git a/source/java/org/alfresco/rest/api/impl/RenditionsImpl.java b/source/java/org/alfresco/rest/api/impl/RenditionsImpl.java index 4aa9b5117e..a8780e431e 100644 --- a/source/java/org/alfresco/rest/api/impl/RenditionsImpl.java +++ b/source/java/org/alfresco/rest/api/impl/RenditionsImpl.java @@ -21,6 +21,7 @@ package org.alfresco.rest.api.impl; import org.alfresco.model.ContentModel; import org.alfresco.query.PagingResults; +import org.alfresco.repo.tenant.TenantService; import org.alfresco.repo.thumbnail.ThumbnailDefinition; import org.alfresco.repo.thumbnail.ThumbnailHelper; import org.alfresco.repo.thumbnail.ThumbnailRegistry; @@ -96,6 +97,7 @@ public class RenditionsImpl implements Renditions, ResourceLoaderAware private NamespaceService namespaceService; private ServiceRegistry serviceRegistry; private ResourceLoader resourceLoader; + private TenantService tenantService; public void setNodes(Nodes nodes) { @@ -123,12 +125,18 @@ public class RenditionsImpl implements Renditions, ResourceLoaderAware this.resourceLoader = resourceLoader; } + public void setTenantService(TenantService tenantService) + { + this.tenantService = tenantService; + } + public void init() { PropertyCheck.mandatory(this, "nodes", nodes); PropertyCheck.mandatory(this, "thumbnailService", thumbnailService); PropertyCheck.mandatory(this, "scriptThumbnailService", scriptThumbnailService); PropertyCheck.mandatory(this, "serviceRegistry", serviceRegistry); + PropertyCheck.mandatory(this, "tenantService", tenantService); this.nodeService = serviceRegistry.getNodeService(); this.actionService = serviceRegistry.getActionService(); @@ -288,7 +296,13 @@ public class RenditionsImpl implements Renditions, ResourceLoaderAware public BinaryResource getContent(String nodeId, String renditionId, Parameters parameters) { final NodeRef sourceNodeRef = validateSourceNode(nodeId); - final NodeRef renditionNodeRef = getRenditionByName(sourceNodeRef, renditionId, parameters); + return getContent(sourceNodeRef, renditionId, parameters); + } + + @Override + public BinaryResource getContent(NodeRef sourceNodeRef, String renditionId, Parameters parameters) + { + NodeRef renditionNodeRef = getRenditionByName(sourceNodeRef, renditionId, parameters); // By default set attachment header (with rendition Id) unless attachment=false boolean attach = true; @@ -382,7 +396,8 @@ public class RenditionsImpl implements Renditions, ResourceLoaderAware { return null; } - return nodeRefRendition.getChildRef(); + + return tenantService.getName(nodeRef, nodeRefRendition.getChildRef()); } protected Rendition toApiRendition(NodeRef renditionNodeRef) diff --git a/source/test-java/org/alfresco/rest/api/tests/SharedLinkApiTest.java b/source/test-java/org/alfresco/rest/api/tests/SharedLinkApiTest.java index bae33d0bca..4b3888e49e 100644 --- a/source/test-java/org/alfresco/rest/api/tests/SharedLinkApiTest.java +++ b/source/test-java/org/alfresco/rest/api/tests/SharedLinkApiTest.java @@ -27,6 +27,9 @@ import org.alfresco.rest.api.impl.QuickShareLinksImpl; import org.alfresco.rest.api.model.QuickShareLink; import org.alfresco.rest.api.nodes.NodesEntityResource; import org.alfresco.rest.api.quicksharelinks.QuickShareLinkEntityResource; +import org.alfresco.rest.api.tests.RepoService.TestNetwork; +import org.alfresco.rest.api.tests.RepoService.TestPerson; +import org.alfresco.rest.api.tests.RepoService.TestSite; import org.alfresco.rest.api.tests.client.HttpResponse; import org.alfresco.rest.api.tests.client.PublicApiClient.Paging; import org.alfresco.rest.api.tests.client.data.Document; @@ -35,8 +38,10 @@ import org.alfresco.rest.api.tests.client.data.QuickShareLinkEmailRequest; import org.alfresco.rest.api.tests.client.data.Rendition; import org.alfresco.rest.api.tests.util.MultiPartBuilder; import org.alfresco.rest.api.tests.util.RestApiUtil; +import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.security.MutableAuthenticationService; import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.cmr.site.SiteVisibility; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -68,6 +73,19 @@ public class SharedLinkApiTest extends AbstractBaseApiTest { private static final String URL_SHARED_LINKS = "shared-links"; + TestNetwork networkOne; + + /** + * User one from network one + */ + private TestPerson userOneN1; + + /** + * User two from network one + */ + private TestPerson userTwoN1; + private TestSite userOneN1Site; + private String user1; private String user2; private List users = new ArrayList<>(); @@ -91,6 +109,12 @@ public class SharedLinkApiTest extends AbstractBaseApiTest // so the tests for the specific network would work. users.add(user1); users.add(user2); + + networkOne = getTestFixture().getRandomNetwork(); + userOneN1 = networkOne.createUser(); + userTwoN1 = networkOne.createUser(); + + userOneN1Site = createSite(networkOne, userOneN1, SiteVisibility.PRIVATE); } @After @@ -131,6 +155,7 @@ public class SharedLinkApiTest extends AbstractBaseApiTest * {@literal :/alfresco/api//public/alfresco/versions/1/shared-links/} * {@literal :/alfresco/api//public/alfresco/versions/1/shared-links//content} * {@literal :/alfresco/api//public/alfresco/versions/1/shared-links//renditions} + * {@literal :/alfresco/api//public/alfresco/versions/1/shared-links//renditions//content} * */ @Test @@ -661,6 +686,166 @@ public class SharedLinkApiTest extends AbstractBaseApiTest } } + /** + * Tests shared links to file (content) in a multi-tenant system. + * + *

POST:

+ * {@literal :/alfresco/api//public/alfresco/versions/1/shared-links} + * + *

DELETE:

+ * {@literal :/alfresco/api//public/alfresco/versions/1/shared-links/} + * + *

GET:

+ * The following do not require authentication + * {@literal :/alfresco/api//public/alfresco/versions/1/shared-links/} + * {@literal :/alfresco/api//public/alfresco/versions/1/shared-links//content} + * {@literal :/alfresco/api//public/alfresco/versions/1/shared-links//renditions} + * {@literal :/alfresco/api//public/alfresco/versions/1/shared-links//renditions//content} + * + */ + @Test + public void testSharedLinkCreateGetDelete_MultiTenant() throws Exception + { + // As userOneN1 + AuthenticationUtil.setFullyAuthenticatedUser(userOneN1.getId()); + NodeRef docLibNodeRef = userOneN1Site.getContainerNodeRef(("documentLibrary")); + String docLibNodeId = docLibNodeRef.getId(); + + String folderName = "folder" + System.currentTimeMillis() + "_1"; + String folderId = createFolder(userOneN1.getId(), docLibNodeId, folderName, null).getId(); + + // create doc d1 - pdf + String fileName1 = "quick" + RUNID + "_1.pdf"; + File file1 = getResourceFile("quick.pdf"); + + byte[] file1_originalBytes = Files.readAllBytes(Paths.get(file1.getAbsolutePath())); + + String file1_MimeType = MimetypeMap.MIMETYPE_PDF; + + MultiPartBuilder.MultiPartRequest reqBody = MultiPartBuilder.create() + .setFileData(new MultiPartBuilder.FileData(fileName1, file1, file1_MimeType)) + .build(); + + HttpResponse response = post(getNodeChildrenUrl(folderId), userOneN1.getId(), reqBody.getBody(), null, reqBody.getContentType(), 201); + Document doc1 = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), Document.class); + String d1Id = doc1.getId(); + assertNotNull(d1Id); + + // create shared link to document 1 + Map body = new HashMap<>(); + body.put("nodeId", d1Id); + response = post(URL_SHARED_LINKS, userOneN1.getId(), toJsonAsStringNonNull(body), 201); + QuickShareLink resp = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), QuickShareLink.class); + String shared1Id = resp.getId(); + assertNotNull(shared1Id); + assertEquals(d1Id, resp.getNodeId()); + assertEquals(fileName1, resp.getName()); + assertEquals(file1_MimeType, resp.getContent().getMimeType()); + assertEquals(userOneN1.getId(), resp.getSharedByUser().getId()); + + // allowable operations not included - no params + response = getSingle(QuickShareLinkEntityResource.class, userOneN1.getId(), shared1Id, null, 200); + resp = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), QuickShareLink.class); + assertNull(resp.getAllowableOperations()); + + // unauth access to get shared link info + Map params = Collections.singletonMap("include", "allowableOperations"); // note: this will be ignore for unauth access + response = getSingle(QuickShareLinkEntityResource.class, null, shared1Id, params, 200); + resp = RestApiUtil.parseRestApiEntry(response.getJsonResponse(), QuickShareLink.class); + assertEquals(shared1Id, resp.getId()); + assertEquals(fileName1, resp.getName()); + assertEquals(d1Id, resp.getNodeId()); + assertNull(resp.getAllowableOperations()); // include is ignored + + // unauth access to file 1 content (via shared link) + response = getSingle(QuickShareLinkEntityResource.class, null, shared1Id + "/content", null, 200); + assertArrayEquals(file1_originalBytes, response.getResponseAsBytes()); + Map responseHeaders = response.getHeaders(); + assertNotNull(responseHeaders); + assertEquals(file1_MimeType + ";charset=UTF-8", responseHeaders.get("Content-Type")); + assertNotNull(responseHeaders.get("Expires")); + assertEquals("attachment; filename=\"" + fileName1 + "\"; filename*=UTF-8''" + fileName1 + "", responseHeaders.get("Content-Disposition")); + String lastModifiedHeader = responseHeaders.get(LAST_MODIFIED_HEADER); + assertNotNull(lastModifiedHeader); + // Test 304 response + Map headers = Collections.singletonMap(IF_MODIFIED_SINCE_HEADER, lastModifiedHeader); + getSingle(URL_SHARED_LINKS, null, shared1Id + "/content", null, headers, 304); + + // unauth access to file 1 content (via shared link) - without Content-Disposition header (attachment=false) + params = new HashMap<>(); + params.put("attachment", "false"); + response = getSingle(QuickShareLinkEntityResource.class, null, shared1Id + "/content", params, 200); + assertArrayEquals(file1_originalBytes, response.getResponseAsBytes()); + responseHeaders = response.getHeaders(); + assertNotNull(responseHeaders); + assertEquals(file1_MimeType + ";charset=UTF-8", responseHeaders.get("Content-Type")); + assertNotNull(responseHeaders.get(LAST_MODIFIED_HEADER)); + assertNotNull(responseHeaders.get("Expires")); + assertNull(responseHeaders.get("Content-Disposition")); + + // -ve shared link rendition tests + { + // -ve test - try to get non-existent rendition content + getSingle(QuickShareLinkEntityResource.class, null, shared1Id + "/renditions/doclib/content", null, 404); + + // -ve test - try to get unregistered rendition content + getSingle(QuickShareLinkEntityResource.class, null, shared1Id + "/renditions/dummy/content", null, 404); + } + + // unauth access to get shared link renditions info (available => CREATED renditions only) + response = getAll(URL_SHARED_LINKS + "/" + shared1Id + "/renditions", null, null, 200); + List renditions = RestApiUtil.parseRestApiEntries(response.getJsonResponse(), Rendition.class); + assertEquals(0, renditions.size()); + + // create rendition of pdf doc - note: for some reason create rendition of txt doc fail on build m/c (TBC) ? + Rendition rendition = createAndGetRendition(userOneN1.getId(), d1Id, "doclib"); + assertNotNull(rendition); + assertEquals(Rendition.RenditionStatus.CREATED, rendition.getStatus()); + + // unauth access to get shared link renditions info (available => CREATED renditions only) + response = getAll(URL_SHARED_LINKS + "/" + shared1Id + "/renditions", null, null, 200); + renditions = RestApiUtil.parseRestApiEntries(response.getJsonResponse(), Rendition.class); + assertEquals(1, renditions.size()); + assertEquals(Rendition.RenditionStatus.CREATED, renditions.get(0).getStatus()); + assertEquals("doclib", renditions.get(0).getId()); + + // unauth access to get shared link file rendition content + response = getSingle(QuickShareLinkEntityResource.class, null, shared1Id + "/renditions/doclib/content", null, 200); + assertTrue(response.getResponseAsBytes().length > 0); + responseHeaders = response.getHeaders(); + assertNotNull(responseHeaders); + assertEquals(MimetypeMap.MIMETYPE_IMAGE_PNG + ";charset=UTF-8", responseHeaders.get("Content-Type")); + assertNotNull(responseHeaders.get(LAST_MODIFIED_HEADER)); + assertNotNull(responseHeaders.get("Expires")); + String docName = "doclib"; + assertEquals("attachment; filename=\"" + docName + "\"; filename*=UTF-8''" + docName + "", responseHeaders.get("Content-Disposition")); + + // unauth access to get shared link file rendition content - without Content-Disposition header (attachment=false) + params = new HashMap<>(); + params.put("attachment", "false"); + response = getSingle(QuickShareLinkEntityResource.class, null, shared1Id + "/renditions/doclib/content", params, 200); + assertTrue(response.getResponseAsBytes().length > 0); + responseHeaders = response.getHeaders(); + assertNotNull(responseHeaders); + assertEquals(MimetypeMap.MIMETYPE_IMAGE_PNG + ";charset=UTF-8", responseHeaders.get("Content-Type")); + assertNotNull(responseHeaders.get("Expires")); + assertNull(responseHeaders.get("Content-Disposition")); + lastModifiedHeader = responseHeaders.get(LAST_MODIFIED_HEADER); + assertNotNull(lastModifiedHeader); + // Test 304 response + headers = Collections.singletonMap(IF_MODIFIED_SINCE_HEADER, lastModifiedHeader); + getSingle(URL_SHARED_LINKS, null, shared1Id + "/renditions/doclib/content", null, headers, 304); + + // -ve test - userTwoN1 cannot delete shared link + delete(URL_SHARED_LINKS, userTwoN1.getId(), shared1Id, 403); + + // -ve test - unauthenticated + delete(URL_SHARED_LINKS, null, shared1Id, 401); + + // delete shared link + delete(URL_SHARED_LINKS, userOneN1.getId(), shared1Id, 204); + } + @Override public String getScope() {