From 6fed14733c1f16d5147695904b7cc1f27e017597 Mon Sep 17 00:00:00 2001 From: Alex Mukha Date: Wed, 23 Aug 2017 11:25:47 +0100 Subject: [PATCH] Merge branch 'feature/REPO-1851_get_avatar' into 'develop' Avatars: retrieve, update and delete See merge request !23 --- .../java/org/alfresco/rest/api/People.java | 31 +- .../org/alfresco/rest/api/Renditions.java | 36 +- .../alfresco/rest/api/impl/PeopleImpl.java | 181 +++++++-- .../rest/api/impl/RenditionsImpl.java | 26 +- .../rest/api/people/PeopleEntityResource.java | 72 +++- .../alfresco/public-rest-context.xml | 1 + .../alfresco/rest/api/tests/TestPeople.java | 373 +++++++++++++++++- .../api/tests/client/PublicApiClient.java | 79 +++- .../api/tests/client/PublicApiHttpClient.java | 22 +- .../rest/api/tests/client/data/Avatar.java | 44 +++ 10 files changed, 794 insertions(+), 71 deletions(-) create mode 100644 src/test/java/org/alfresco/rest/api/tests/client/data/Avatar.java diff --git a/src/main/java/org/alfresco/rest/api/People.java b/src/main/java/org/alfresco/rest/api/People.java index c319914adb..b6daf9628b 100644 --- a/src/main/java/org/alfresco/rest/api/People.java +++ b/src/main/java/org/alfresco/rest/api/People.java @@ -25,15 +25,18 @@ */ package org.alfresco.rest.api; +import java.io.InputStream; +import java.util.List; + import org.alfresco.rest.api.model.PasswordReset; import org.alfresco.rest.api.model.Person; +import org.alfresco.rest.framework.resource.content.BasicContentInfo; +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; import org.alfresco.service.cmr.security.NoSuchPersonException; -import java.util.List; - public interface People { String DEFAULT_USER = "-me-"; @@ -102,4 +105,28 @@ public interface People * @param passwordReset the password reset details */ void resetPassword(String personId, PasswordReset passwordReset); + + /** + * + * @param personId + * @param parameters + * @return + */ + BinaryResource downloadAvatarContent(String personId, Parameters parameters); + + /** + * + * @param personId + * @param contentInfo + * @param stream + * @param parameters + * @return + */ + Person uploadAvatarContent(String personId, BasicContentInfo contentInfo, InputStream stream, Parameters parameters); + + /** + * + * @param personId + */ + void deleteAvatarContent(String personId); } diff --git a/src/main/java/org/alfresco/rest/api/Renditions.java b/src/main/java/org/alfresco/rest/api/Renditions.java index bd3bded41a..2e4cb687fa 100644 --- a/src/main/java/org/alfresco/rest/api/Renditions.java +++ b/src/main/java/org/alfresco/rest/api/Renditions.java @@ -30,7 +30,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; +import org.alfresco.service.cmr.repository.NodeRef; /** * Renditions API @@ -39,8 +39,8 @@ import org.alfresco.service.cmr.repository.NodeRef; */ public interface Renditions { - String PARAM_STATUS = "status"; - + String PARAM_STATUS = "status"; + /** * Lists all available renditions includes those that have been created and those that are yet to be created. * @@ -70,6 +70,16 @@ public interface Renditions */ void createRendition(String nodeId, Rendition rendition, Parameters parameters); + /** + * Creates a rendition for the given node - either async r sync + * + * @param nodeId + * @param rendition + * @param executeAsync + * @param parameters + */ + void createRendition(String nodeId, Rendition rendition, boolean executeAsync, Parameters parameters); + /** * Downloads rendition. * @@ -80,14 +90,14 @@ public interface Renditions */ BinaryResource getContent(String nodeId, String renditionId, Parameters parameters); - /** - * 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); + /** + * 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/src/main/java/org/alfresco/rest/api/impl/PeopleImpl.java b/src/main/java/org/alfresco/rest/api/impl/PeopleImpl.java index 67c49cb3a1..10e09246fe 100644 --- a/src/main/java/org/alfresco/rest/api/impl/PeopleImpl.java +++ b/src/main/java/org/alfresco/rest/api/impl/PeopleImpl.java @@ -37,18 +37,25 @@ import org.alfresco.repo.security.authentication.ResetPasswordServiceImpl.ResetP import org.alfresco.repo.security.authentication.ResetPasswordServiceImpl.ResetPasswordWorkflowInvalidUserException; import org.alfresco.rest.api.Nodes; import org.alfresco.rest.api.People; +import org.alfresco.rest.api.Renditions; import org.alfresco.rest.api.Sites; +import org.alfresco.rest.api.model.Node; import org.alfresco.rest.api.model.PasswordReset; import org.alfresco.rest.api.model.Person; +import org.alfresco.rest.api.model.Rendition; import org.alfresco.rest.framework.core.exceptions.ConstraintViolatedException; +import org.alfresco.rest.framework.core.exceptions.DisabledServiceException; import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; import org.alfresco.rest.framework.core.exceptions.PermissionDeniedException; +import org.alfresco.rest.framework.resource.content.BasicContentInfo; +import org.alfresco.rest.framework.resource.content.BinaryResource; import org.alfresco.rest.framework.resource.parameters.CollectionWithPagingInfo; import org.alfresco.rest.framework.resource.parameters.Paging; import org.alfresco.rest.framework.resource.parameters.Parameters; import org.alfresco.rest.framework.resource.parameters.SortColumn; import org.alfresco.service.cmr.repository.AssociationRef; +import org.alfresco.service.cmr.repository.ChildAssociationRef; import org.alfresco.service.cmr.repository.ContentData; import org.alfresco.service.cmr.repository.ContentReader; import org.alfresco.service.cmr.repository.ContentService; @@ -65,6 +72,7 @@ import org.alfresco.util.Pair; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import java.io.InputStream; import java.io.Serializable; import java.util.AbstractList; import java.util.ArrayList; @@ -97,9 +105,9 @@ public class PeopleImpl implements People PermissionService.GROUP_PREFIX, PermissionService.ROLE_PREFIX }; + protected Nodes nodes; protected Sites sites; - protected SiteService siteService; protected NodeService nodeService; protected PersonService personService; @@ -109,6 +117,7 @@ public class PeopleImpl implements People protected ContentService contentService; protected ThumbnailService thumbnailService; protected ResetPasswordService resetPasswordService; + protected Renditions renditions; private final static Map sort_params_to_qnames; static @@ -175,6 +184,12 @@ public class PeopleImpl implements People this.resetPasswordService = resetPasswordService; } + public void setRenditions(Renditions renditions) + { + this.renditions = renditions; + } + + /** * Validate, perform -me- substitution and canonicalize the person ID. * @@ -257,53 +272,134 @@ public class PeopleImpl implements People public boolean hasAvatar(NodeRef personNodeRef) { - if(personNodeRef != null) - { - List avatorAssocs = nodeService.getTargetAssocs(personNodeRef, ContentModel.ASSOC_AVATAR); - return(avatorAssocs.size() > 0); - } - else - { - return false; - } + return (getAvatarOriginal(personNodeRef) != null); } @Override public NodeRef getAvatar(String personId) { NodeRef avatar = null; - personId = validatePerson(personId); NodeRef personNode = personService.getPerson(personId); if(personNode != null) { - List avatorAssocs = nodeService.getTargetAssocs(personNode, ContentModel.ASSOC_AVATAR); - if(avatorAssocs.size() > 0) - { - AssociationRef ref = avatorAssocs.get(0); - NodeRef thumbnailNodeRef = thumbnailService.getThumbnailByName(ref.getTargetRef(), ContentModel.PROP_CONTENT, "avatar"); - if(thumbnailNodeRef != null) - { - avatar = thumbnailNodeRef; - } - else - { - throw new EntityNotFoundException("avatar"); - } - } - else - { - throw new EntityNotFoundException("avatar"); - } + NodeRef avatarOrig = getAvatarOriginal(personNode); + avatar = thumbnailService.getThumbnailByName(avatarOrig, ContentModel.PROP_CONTENT, "avatar"); } - else + + if (avatar == null) { throw new EntityNotFoundException(personId); } - + return avatar; } + private NodeRef getAvatarOriginal(NodeRef personNode) + { + NodeRef avatarOrigNodeRef = null; + List avatarChildAssocs = nodeService.getChildAssocs(personNode, Collections.singleton(ContentModel.ASSOC_PREFERENCE_IMAGE)); + if (avatarChildAssocs.size() > 0) + { + ChildAssociationRef ref = avatarChildAssocs.get(0); + avatarOrigNodeRef = ref.getChildRef(); + } + else + { + // TODO do we still need this ? - backward compatible with JSF web-client avatar + List avatorAssocs = nodeService.getTargetAssocs(personNode, ContentModel.ASSOC_AVATAR); + if (avatorAssocs.size() > 0) + { + AssociationRef ref = avatorAssocs.get(0); + avatarOrigNodeRef = ref.getTargetRef(); + } + } + return avatarOrigNodeRef; + } + + @Override + public BinaryResource downloadAvatarContent(String personId, Parameters parameters) + { + personId = validatePerson(personId); + NodeRef personNode = personService.getPerson(personId); + NodeRef avatarNodeRef = getAvatarOriginal(personNode); + + return renditions.getContent(avatarNodeRef, "avatar", parameters); + } + + @Override + public Person uploadAvatarContent(String personId, BasicContentInfo contentInfo, InputStream stream, Parameters parameters) + { + if (!thumbnailService.getThumbnailsEnabled()) + { + throw new DisabledServiceException("Thumbnail generation has been disabled."); + } + + personId = validatePerson(personId); + checkCurrentUserOrAdmin(personId); + + NodeRef personNode = personService.getPerson(personId); + NodeRef avatarOrigNodeRef = getAvatarOriginal(personNode); + + if (avatarOrigNodeRef != null) + { + deleteAvatar(avatarOrigNodeRef); + } + + QName origAvatarQName = QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "origAvatar"); + nodeService.addAspect(personNode, ContentModel.ASPECT_PREFERENCES, null); + ChildAssociationRef assoc = nodeService.createNode(personNode, ContentModel.ASSOC_PREFERENCE_IMAGE, origAvatarQName, + ContentModel.TYPE_CONTENT); + NodeRef avatar = assoc.getChildRef(); + String avatarOriginalNodeId = avatar.getId(); + + // TODO do we still need this ? - backward compatible with JSF web-client avatar + nodeService.createAssociation(personNode, avatar, ContentModel.ASSOC_AVATAR); + + Node n = nodes.updateContent(avatarOriginalNodeId, contentInfo, stream, parameters); + String mimeType = n.getContent().getMimeType(); + + if (mimeType.indexOf("image/") != 0) + { + throw new InvalidArgumentException( + "Uploaded content must be an image (content type determined to be '"+mimeType+"')"); + } + + // create thumbnail synchronously + Rendition avatarR = new Rendition(); + avatarR.setId("avatar"); + renditions.createRendition(avatarOriginalNodeId, avatarR, false, parameters); + + List include = Arrays.asList( + PARAM_INCLUDE_ASPECTNAMES, + PARAM_INCLUDE_PROPERTIES); + + return getPersonWithProperties(personId, include); + } + + @Override + public void deleteAvatarContent(String personId) + { + personId = validatePerson(personId); + checkCurrentUserOrAdmin(personId); + + NodeRef personNode = personService.getPerson(personId); + NodeRef avatarOrigNodeRef = getAvatarOriginal(personNode); + if (avatarOrigNodeRef != null) + { + deleteAvatar(avatarOrigNodeRef); + } + } + + private void deleteAvatar(NodeRef avatarOrigNodeRef) + { + // Set as temporary to permanently delete node (instead of archiving) + nodeService.addAspect(avatarOrigNodeRef, ContentModel.ASPECT_TEMPORARY, null); + + nodeService.deleteNode(avatarOrigNodeRef); + } + + /** * Get a full representation of a person. * @@ -612,14 +708,8 @@ public class PeopleImpl implements People personId = validatePerson(personId); validateUpdatePersonData(person); - boolean isAdmin = isAdminAuthority(); - - String currentUserId = AuthenticationUtil.getFullyAuthenticatedUser(); - if (!isAdmin && !currentUserId.equalsIgnoreCase(personId)) - { - // The user is not an admin user and is not attempting to update *their own* details. - throw new PermissionDeniedException(); - } + // Check if user updating *their own* details or is an admin + boolean isAdmin = checkCurrentUserOrAdmin(personId); final String personIdToUpdate = validatePerson(personId); final Map properties = person.toProperties(); @@ -675,6 +765,19 @@ public class PeopleImpl implements People return getPerson(personId); } + private boolean checkCurrentUserOrAdmin(String personId) + { + boolean isAdmin = isAdminAuthority(); + + String currentUserId = AuthenticationUtil.getFullyAuthenticatedUser(); + if (!isAdmin && !currentUserId.equalsIgnoreCase(personId)) + { + throw new PermissionDeniedException(); + } + + return isAdmin; + } + private void validateUpdatePersonData(Person person) { validateNamespaces(person.getAspectNames(), person.getProperties()); diff --git a/src/main/java/org/alfresco/rest/api/impl/RenditionsImpl.java b/src/main/java/org/alfresco/rest/api/impl/RenditionsImpl.java index b2bfebbf63..8c804e913c 100644 --- a/src/main/java/org/alfresco/rest/api/impl/RenditionsImpl.java +++ b/src/main/java/org/alfresco/rest/api/impl/RenditionsImpl.java @@ -260,6 +260,12 @@ public class RenditionsImpl implements Renditions, ResourceLoaderAware @Override public void createRendition(String nodeId, Rendition rendition, Parameters parameters) + { + createRendition(nodeId, rendition, true, parameters); + } + + @Override + public void createRendition(String nodeId, Rendition rendition, boolean executeAsync, Parameters parameters) { // If thumbnail generation has been configured off, then don't bother. if (!thumbnailService.getThumbnailsEnabled()) @@ -292,8 +298,9 @@ public class RenditionsImpl implements Renditions, ResourceLoaderAware } Action action = ThumbnailHelper.createCreateThumbnailAction(thumbnailDefinition, serviceRegistry); - // Queue async creation of thumbnail - actionService.executeAction(action, sourceNodeRef, true, true); + + // Create thumbnail - or else queue for async creation + actionService.executeAction(action, sourceNodeRef, true, executeAsync); } @Override @@ -324,7 +331,15 @@ public class RenditionsImpl implements Renditions, ResourceLoaderAware { throw new NotFoundException("Thumbnail was not found for [" + renditionId + ']'); } - String sourceNodeMimeType = getMimeType(sourceNodeRef); + String sourceNodeMimeType = null; + try + { + sourceNodeMimeType = (sourceNodeRef != null ? getMimeType(sourceNodeRef) : null); + } + catch (InvalidArgumentException e) + { + // No content for node, e.g. ASSOC_AVATAR rather than ASSOC_PREFERENCE_IMAGE + } // resource based on the content's mimeType and rendition id String phPath = scriptThumbnailService.getMimeAwarePlaceHolderResourcePath(renditionId, sourceNodeMimeType); if (phPath == null) @@ -388,6 +403,11 @@ public class RenditionsImpl implements Renditions, ResourceLoaderAware protected NodeRef getRenditionByName(NodeRef nodeRef, String renditionId, Parameters parameters) { + if (nodeRef == null) + { + return null; + } + if (StringUtils.isEmpty(renditionId)) { throw new InvalidArgumentException("renditionId can't be null or empty."); diff --git a/src/main/java/org/alfresco/rest/api/people/PeopleEntityResource.java b/src/main/java/org/alfresco/rest/api/people/PeopleEntityResource.java index df40c3fada..bf5af1df97 100644 --- a/src/main/java/org/alfresco/rest/api/people/PeopleEntityResource.java +++ b/src/main/java/org/alfresco/rest/api/people/PeopleEntityResource.java @@ -25,19 +25,30 @@ */ package org.alfresco.rest.api.people; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +import javax.servlet.http.HttpServletResponse; + import org.alfresco.model.ContentModel; import org.alfresco.rest.api.People; import org.alfresco.rest.api.model.Client; import org.alfresco.rest.api.model.PasswordReset; import org.alfresco.rest.api.model.Person; +import org.alfresco.rest.framework.BinaryProperties; import org.alfresco.rest.framework.Operation; import org.alfresco.rest.framework.WebApiDescription; import org.alfresco.rest.framework.WebApiNoAuth; import org.alfresco.rest.framework.WebApiParam; import org.alfresco.rest.framework.core.ResourceParameter; +import org.alfresco.rest.framework.core.exceptions.EntityNotFoundException; import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException; import org.alfresco.rest.framework.resource.EntityResource; +import org.alfresco.rest.framework.resource.actions.interfaces.BinaryResourceAction; import org.alfresco.rest.framework.resource.actions.interfaces.EntityResourceAction; +import org.alfresco.rest.framework.resource.content.BasicContentInfo; +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.rest.framework.webscripts.WithResponse; @@ -46,10 +57,6 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.InitializingBean; -import javax.servlet.http.HttpServletResponse; -import java.util.ArrayList; -import java.util.List; - /** * An implementation of an Entity Resource for a Person * @@ -57,12 +64,15 @@ import java.util.List; * @author Gethin James */ @EntityResource(name="people", title = "People") -public class PeopleEntityResource implements EntityResourceAction.ReadById, EntityResourceAction.Create, EntityResourceAction.Update,EntityResourceAction.Read, InitializingBean +public class PeopleEntityResource implements EntityResourceAction.ReadById, EntityResourceAction.Create, + EntityResourceAction.Update,EntityResourceAction.Read, + + BinaryResourceAction.Read, BinaryResourceAction.Update, BinaryResourceAction.Delete, InitializingBean { private static Log logger = LogFactory.getLog(PeopleEntityResource.class); private People people; - + public void setPeople(People people) { this.people = people; @@ -169,4 +179,54 @@ public class PeopleEntityResource implements EntityResourceAction.ReadById + diff --git a/src/test/java/org/alfresco/rest/api/tests/TestPeople.java b/src/test/java/org/alfresco/rest/api/tests/TestPeople.java index 5d1a3b16f4..14000dc273 100644 --- a/src/test/java/org/alfresco/rest/api/tests/TestPeople.java +++ b/src/test/java/org/alfresco/rest/api/tests/TestPeople.java @@ -26,12 +26,15 @@ package org.alfresco.rest.api.tests; import org.alfresco.model.ContentModel; +import org.alfresco.repo.content.ContentLimitProvider; import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.security.authentication.ResetPasswordServiceImpl; +import org.alfresco.rest.api.Renditions; import org.alfresco.rest.api.model.Client; import org.alfresco.rest.api.model.LoginTicket; import org.alfresco.rest.api.model.LoginTicketResponse; import org.alfresco.rest.api.model.PasswordReset; +import org.alfresco.rest.api.model.Rendition; import org.alfresco.rest.api.tests.RepoService.TestNetwork; import org.alfresco.rest.api.tests.client.HttpResponse; import org.alfresco.rest.api.tests.client.Pair; @@ -42,21 +45,32 @@ import org.alfresco.rest.api.tests.client.RequestContext; import org.alfresco.rest.api.tests.client.data.Company; import org.alfresco.rest.api.tests.client.data.JSONAble; import org.alfresco.rest.api.tests.client.data.Person; -import org.alfresco.util.email.EmailUtil; import org.alfresco.rest.api.tests.util.RestApiUtil; import org.alfresco.service.cmr.preference.PreferenceService; +import org.alfresco.service.cmr.repository.AssociationRef; +import org.alfresco.service.cmr.repository.ChildAssociationRef; +import org.alfresco.service.cmr.repository.ContentService; +import org.alfresco.service.cmr.repository.ContentWriter; import org.alfresco.service.cmr.repository.NodeRef; import org.alfresco.service.cmr.repository.NodeService; import org.alfresco.service.cmr.security.MutableAuthenticationService; import org.alfresco.service.cmr.security.PersonService; +import org.alfresco.service.cmr.thumbnail.ThumbnailService; +import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; import org.alfresco.util.GUID; +import org.alfresco.util.email.EmailUtil; import org.apache.commons.httpclient.HttpStatus; import org.json.simple.JSONObject; import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.springframework.core.io.ClassPathResource; +import javax.mail.internet.MimeMessage; +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.IOException; import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; @@ -69,8 +83,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; - -import javax.mail.internet.MimeMessage; +import java.util.stream.Collectors; import static org.alfresco.repo.security.authentication.ResetPasswordServiceImplTest.getWorkflowIdAndKeyFromUrl; import static org.junit.Assert.assertEquals; @@ -1920,6 +1933,360 @@ public class TestPeople extends AbstractBaseApiTest return URL_PEOPLE + '/' + userId + "/reset-password"; } + + @Test + public void retrieveAvatar() throws Exception + { + final String person1 = account1PersonIt.next(); + publicApiClient.setRequestContext(new RequestContext(account1.getId(), person1)); + AuthenticationUtil.setFullyAuthenticatedUser(person1); + NodeRef person1Ref = personService.getPerson(person1, false); + + // No avatar, but valid person + { + deleteAvatarDirect(person1Ref); + assertNotNull(people.getPerson(person1)); // Pre-condition of test case + people.getAvatar(person1, false, 404); + } + + // No avatar, but person exists and placeholder requested + { + assertNotNull(people.getPerson(person1)); // Pre-condition of test case + people.getAvatar(person1, true, 200); + } + + // Non-existent person + { + String nonPerson = "i-do-not-exist"; + people.getPerson(nonPerson, 404); // Pre-condition of test case + people.getAvatar(nonPerson, false, 404); + } + + // Placeholder requested, but non-existent person + { + String nonPerson = "i-do-not-exist"; + people.getPerson(nonPerson, 404); // Pre-condition of test case + people.getAvatar(nonPerson, true, 404); + } + + // Avatar exists + { + // Create avatar - direct (i.e. not using the API, so that tests for get avatar can be separated from upload) + // There's no significance to the image being used here, it was the most suitable I could find. + ClassPathResource thumbRes = new ClassPathResource("test.jpg"); + deleteAvatarDirect(person1Ref); + createAvatarDirect(person1Ref, thumbRes.getFile()); + + // Get avatar - API call + people.getAvatar(person1, false, 200); + } + + // -me- alias + { + people.getAvatar("-me-", false, 200); + } + + // If-Modified-Since behaviour + { + HttpResponse response = people.getAvatar(person1, false, 200); + Map responseHeaders = response.getHeaders(); + + // Test 304 response + String lastModified = responseHeaders.get(LAST_MODIFIED_HEADER); + assertNotNull(lastModified); + + // Has it been modified since the time it was last modified - no! + people.getAvatar(person1, lastModified, 304); + + // Create an updated avatar + waitMillis(2000); // ensure time has passed between updates + ClassPathResource thumbRes = new ClassPathResource("publicapi/upload/quick.jpg"); + deleteAvatarDirect(person1Ref); + createAvatarDirect(person1Ref, thumbRes.getFile()); + + people.getAvatar(person1, lastModified, 200); + } + + // Attachment param + { + // No attachment parameter (default true) + Boolean attachmentParam = null; + HttpResponse response = people.getAvatar(person1, attachmentParam, false, null, 200); + Map responseHeaders = response.getHeaders(); + String contentDisposition = responseHeaders.get("Content-Disposition"); + assertNotNull(contentDisposition); + assertTrue(contentDisposition.startsWith("attachment;")); + + // attachment=true + attachmentParam = true; + response = people.getAvatar(person1, attachmentParam, false, null, 200); + responseHeaders = response.getHeaders(); + contentDisposition = responseHeaders.get("Content-Disposition"); + assertNotNull(contentDisposition); + assertTrue(contentDisposition.startsWith("attachment;")); + + // attachment=false + attachmentParam = false; + response = people.getAvatar(person1, attachmentParam, false, null, 200); + responseHeaders = response.getHeaders(); + contentDisposition = responseHeaders.get("Content-Disposition"); + assertNull(contentDisposition); + } + } + + private void waitMillis(int requiredDelay) + { + long startTime = System.currentTimeMillis(); + long currTime = startTime; + while (currTime < (startTime + requiredDelay)) + { + try + { + Thread.sleep(requiredDelay); + } + catch (InterruptedException e) + { + System.out.println(":>>> " + e.getMessage()); + } + finally + { + currTime = System.currentTimeMillis(); + System.out.println(":>>> waited "+(currTime-startTime) + "ms"); + } + } + } + + private void deleteAvatarDirect(NodeRef personRef) + { + + List assocs = nodeService.getChildAssocs(personRef). + stream(). + filter(x -> x.getTypeQName().equals(ContentModel.ASSOC_PREFERENCE_IMAGE)). + collect(Collectors.toList()); + if (assocs.size() > 0) + { + nodeService.deleteNode(assocs.get(0).getChildRef()); + } + + // remove old association if it exists + List refs = nodeService.getTargetAssocs(personRef, ContentModel.ASSOC_AVATAR); + if (refs.size() == 1) + { + NodeRef existingRef = refs.get(0).getTargetRef(); + nodeService.removeAssociation( + personRef, existingRef, ContentModel.ASSOC_AVATAR); + } + + if (assocs.size() > 1 || refs.size() > 1) + { + fail(String.format("Pref images: %d, Avatar assocs: %d", assocs.size(), refs.size())); + } + } + + private NodeRef createAvatarDirect(NodeRef personRef, File avatarFile) + { + // create new avatar node + nodeService.addAspect(personRef, ContentModel.ASPECT_PREFERENCES, null); + ChildAssociationRef assoc = nodeService.createNode( + personRef, + ContentModel.ASSOC_PREFERENCE_IMAGE, + QName.createQName(NamespaceService.CONTENT_MODEL_1_0_URI, "origAvatar"), + ContentModel.TYPE_CONTENT); + final NodeRef avatarRef = assoc.getChildRef(); + + // JSF client compatibility? + nodeService.createAssociation(personRef, avatarRef, ContentModel.ASSOC_AVATAR); + + // upload the avatar content + ContentService contentService = applicationContext.getBean("ContentService", ContentService.class); + ContentWriter writer = contentService.getWriter(avatarRef, ContentModel.PROP_CONTENT, true); + writer.guessMimetype(avatarFile.getName()); + writer.putContent(avatarFile); + + Rendition avatarR = new Rendition(); + avatarR.setId("avatar"); + Renditions renditions = applicationContext.getBean("Renditions", Renditions.class); + renditions.createRendition(avatarRef.getId(), avatarR, false, null); + + return avatarRef; + } + + @Test + public void updateAvatar() throws PublicApiException, IOException + { + final String person1 = account1PersonIt.next(); + final String person2 = account1PersonIt.next(); + + publicApiClient.setRequestContext(new RequestContext(account1.getId(), person2)); + + AuthenticationUtil.setFullyAuthenticatedUser(person2); + + // Update allowed when no existing avatar + { + // Pre-condition: no avatar exists + NodeRef personRef = personService.getPerson(person2, false); + deleteAvatarDirect(personRef); + people.getAvatar(person2, false, 404); + + // TODO: What do we expect the 200 response body to be? Currently it's the person JSON - doesn't seem right. + ClassPathResource avatar = new ClassPathResource("publicapi/upload/quick.jpg"); + HttpResponse response = people.updateAvatar(person2, avatar.getFile(), 200); + + // TODO: ideally, this should be a "direct" retrieval to isolate update from get + people.getAvatar(person2, false, 200); + } + + // Update existing avatar + { + // Pre-condition: avatar exists + people.getAvatar(person2, false, 200); + + ClassPathResource avatar = new ClassPathResource("test.jpg"); + HttpResponse response = people.updateAvatar(person2, avatar.getFile(), 200); + people.getAvatar(person2, false, 200); + + // -me- alias + people.updateAvatar(person2, avatar.getFile(), 200); + people.getAvatar("-me-", false, 200); + } + + // 400: invalid user ID + { + ClassPathResource avatar = new ClassPathResource("publicapi/upload/quick.jpg"); + people.updateAvatar("joe@@bloggs.example.com", avatar.getFile(), 404); + } + + // 401: authentication failure + { + publicApiClient.setRequestContext(new RequestContext(account1.getId(), account1Admin, "Wr0ngP4ssw0rd!")); + ClassPathResource avatar = new ClassPathResource("publicapi/upload/quick.jpg"); + people.updateAvatar(account1Admin, avatar.getFile(), 401); + } + + // 403: permission denied + { + publicApiClient.setRequestContext(new RequestContext(account1.getId(), person1)); + ClassPathResource avatar = new ClassPathResource("publicapi/upload/quick.jpg"); + people.updateAvatar(person2, avatar.getFile(), 403); + + // Person can update themself + people.updateAvatar(person1, avatar.getFile(), 200); + + // Admin can update someone else + publicApiClient.setRequestContext(new RequestContext(account1.getId(), account1Admin, "admin")); + people.updateAvatar(person1, avatar.getFile(), 200); + } + + // 404: non-existent person + { + publicApiClient.setRequestContext(new RequestContext(account1.getId(), person1)); + // Pre-condition: non-existent person + String nonPerson = "joebloggs@"+account1.getId(); + people.getPerson(nonPerson, 404); + + ClassPathResource avatar = new ClassPathResource("publicapi/upload/quick.jpg"); + people.updateAvatar(nonPerson, avatar.getFile(), 404); + } + + // 413: content exceeds individual file size limit + { + // Test content size limit + final ContentLimitProvider.SimpleFixedLimitProvider limitProvider = applicationContext. + getBean("defaultContentLimitProvider", ContentLimitProvider.SimpleFixedLimitProvider.class); + final long defaultSizeLimit = limitProvider.getSizeLimit(); + limitProvider.setSizeLimitString("20000"); //20 KB + + try + { + ClassPathResource avatar = new ClassPathResource("publicapi/upload/quick.jpg"); // ~26K + people.updateAvatar(person1, avatar.getFile(), 413); + } + finally + { + limitProvider.setSizeLimitString(Long.toString(defaultSizeLimit)); + } + } + + // 501: thumbnails disabled + { + ThumbnailService thumbnailService = applicationContext.getBean("thumbnailService", ThumbnailService.class); + // Disable thumbnail generation + thumbnailService.setThumbnailsEnabled(false); + try + { + ClassPathResource avatar = new ClassPathResource("publicapi/upload/quick.jpg"); + people.updateAvatar(person1, avatar.getFile(), 501); + } + finally + { + thumbnailService.setThumbnailsEnabled(true); + } + } + } + + + @Test + public void removeAvatar() throws IOException, PublicApiException{ + + final String person1 = account1PersonIt.next(); + final String person2 = account1PersonIt.next(); + + publicApiClient.setRequestContext(new RequestContext(account1.getId(), person1)); + + // Avatar exists + { + AuthenticationUtil.setFullyAuthenticatedUser("admin@"+account1.getId()); + + // Create avatar - direct (i.e. not using the API, so that tests for get avatar can be separated from upload) + // There's no significance to the image being used here, it was the most suitable I could find. + ClassPathResource thumbRes = new ClassPathResource("publicapi/upload/quick.jpg"); + NodeRef personRef = personService.getPerson(person1, false); + deleteAvatarDirect(personRef); + createAvatarDirect(personRef, thumbRes.getFile()); + + // Get avatar - API call + people.getAvatar(person1, false, 200); + + //remove avatar avatar exists + people.deleteAvatarImage(person1,204); + + } + + + // Non-existent person + { + String nonPerson = "i-do-not-exist"; + people.getPerson(nonPerson, 404); // Pre-condition of test case + people.deleteAvatarImage(nonPerson, 404); + } + + //Authentication failed 401 + { + setRequestContext(account1.getId(), networkAdmin, "wrongPassword"); + people.deleteAvatarImage(person1,HttpServletResponse.SC_UNAUTHORIZED); + } + + //No permission + { + publicApiClient.setRequestContext(new RequestContext(account1.getId(), person1)); + + AuthenticationUtil.setFullyAuthenticatedUser("admin@"+account1.getId()); + + // Create avatar - direct (i.e. not using the API, so that tests for get avatar can be separated from upload) + // There's no significance to the image being used here, it was the most suitable I could find. + ClassPathResource thumbRes = new ClassPathResource("test.jpg"); + NodeRef personRef = personService.getPerson(person1, false); + deleteAvatarDirect(personRef); + createAvatarDirect(personRef, thumbRes.getFile()); + + people.deleteAvatarImage(person2, 403); + + } + + + + } + @Override public String getScope() { diff --git a/src/test/java/org/alfresco/rest/api/tests/client/PublicApiClient.java b/src/test/java/org/alfresco/rest/api/tests/client/PublicApiClient.java index c33cb12f63..8a456e5faa 100644 --- a/src/test/java/org/alfresco/rest/api/tests/client/PublicApiClient.java +++ b/src/test/java/org/alfresco/rest/api/tests/client/PublicApiClient.java @@ -27,6 +27,7 @@ package org.alfresco.rest.api.tests.client; import static org.junit.Assert.assertNotNull; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.Serializable; @@ -47,7 +48,6 @@ import org.alfresco.rest.api.tests.client.data.AuditEntry; import org.alfresco.rest.api.model.SiteUpdate; import org.alfresco.rest.api.tests.TestPeople; import org.alfresco.rest.api.tests.TestSites; -import org.alfresco.rest.api.tests.client.PublicApiClient.ListResponse; import org.alfresco.rest.api.tests.client.PublicApiHttpClient.BinaryPayload; import org.alfresco.rest.api.tests.client.PublicApiHttpClient.RequestBuilder; import org.alfresco.rest.api.tests.client.data.Activities; @@ -546,15 +546,20 @@ public class PublicApiClient return response; } - public HttpResponse get(String scope, String entityCollectionName, Object entityId, String relationCollectionName, Object relationshipEntityId, Map params) throws IOException + public HttpResponse get(String scope, String entityCollectionName, Object entityId, String relationCollectionName, Object relationshipEntityId, Map params, Map headers) throws IOException { - HttpResponse response = client.get(getRequestContext(), scope, entityCollectionName, entityId, relationCollectionName, relationshipEntityId, params); + HttpResponse response = client.get(getRequestContext(), scope, entityCollectionName, entityId, relationCollectionName, relationshipEntityId, params, headers); logger.debug(response.toString()); return response; } + public HttpResponse get(String scope, String entityCollectionName, Object entityId, String relationCollectionName, Object relationshipEntityId, Map params) throws IOException + { + return get(scope, entityCollectionName, entityId, relationCollectionName, relationshipEntityId, params, null); + } + public HttpResponse getWithPassword(String scope, String password, String entityCollectionName, Object entityId, String relationCollectionName, Object relationshipEntityId, Map params) throws IOException { HttpResponse response = client.get(getRequestContext(), scope, password, entityCollectionName, entityId, relationCollectionName, relationshipEntityId, params); @@ -728,6 +733,21 @@ public class PublicApiClient } } + public HttpResponse getSingle(String entityCollectionName, String entityId, String relationCollectionName, String relationId, Map params, + Map headers, String errorMessage, int expectedStatus) throws PublicApiException + { + try + { + HttpResponse response = get("public", entityCollectionName, entityId, relationCollectionName, relationId, params, headers); + checkStatus(errorMessage, expectedStatus, response); + return response; + } + catch (IOException e) + { + throw new PublicApiException(e); + } + } + public HttpResponse getSingle(String entityCollectionName, String entityId, String relationCollectionName, String relationId, Map params, String errorMessage, int expectedStatus) throws PublicApiException { @@ -1305,6 +1325,59 @@ public class PublicApiClient { remove("people", personId, "activities", String.valueOf(activity.getId()), "Failed to remove activity"); } + + public HttpResponse getAvatar(String personId, boolean placeholder, int expectedStatus) throws PublicApiException + { + return getAvatar(personId, null, placeholder, null, expectedStatus); + } + + public HttpResponse getAvatar(String personId, String ifModifiedSince, int expectedStatus) throws PublicApiException + { + return getAvatar(personId, null, false, ifModifiedSince, expectedStatus); + } + + public HttpResponse getAvatar(String personId, Boolean attachment, boolean placeholder, String ifModifiedSince, int expectedStatus) throws PublicApiException + { + // Binary response expected + Map params = new HashMap<>(); + params.put("placeholder", Boolean.toString(placeholder)); + // Optional attachment parameter + if (attachment != null) + { + params.put("attachment", attachment.toString()); + } + + Map headers = new HashMap<>(); + if (ifModifiedSince != null) + { + headers.put("If-Modified-Since", ifModifiedSince); + } + + HttpResponse response = getSingle("people", personId, "avatar", null, params, headers, "Failed to get avatar", expectedStatus); + checkStatus("Unexpected response", expectedStatus, response); + + return response; + } + + public HttpResponse updateAvatar(String personId, File avatar, int expectedStatus) throws PublicApiException + { + try + { + Map params = new HashMap<>(); + BinaryPayload payload = new BinaryPayload(avatar); + HttpResponse response = client.putBinary(getRequestContext(), "public", 1, "people", personId, "avatar", null, payload, params); + checkStatus("Unexpected status", expectedStatus, response); + return response; + } + catch(IOException e) + { + throw new PublicApiException(e); + } + } + + public void deleteAvatarImage(String personId, int expectedStatus) throws PublicApiException{ + remove("people", personId, "avatar", null, null, "Failed to remove avatar image", expectedStatus); + } } public class Comments extends AbstractProxy diff --git a/src/test/java/org/alfresco/rest/api/tests/client/PublicApiHttpClient.java b/src/test/java/org/alfresco/rest/api/tests/client/PublicApiHttpClient.java index 92424c370f..3e3c840991 100644 --- a/src/test/java/org/alfresco/rest/api/tests/client/PublicApiHttpClient.java +++ b/src/test/java/org/alfresco/rest/api/tests/client/PublicApiHttpClient.java @@ -34,6 +34,7 @@ import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.text.MessageFormat; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; @@ -312,17 +313,34 @@ public class PublicApiHttpClient public HttpResponse get(final RequestContext rq, String scope, final String entityCollectionName, final Object entityId, final String relationCollectionName, final Object relationshipEntityId, Map params) throws IOException { - return get(rq, scope, 1, entityCollectionName, entityId, relationCollectionName, relationshipEntityId, params); + return get(rq, scope, 1, entityCollectionName, entityId, relationCollectionName, relationshipEntityId, params, null); + } + + public HttpResponse get(final RequestContext rq, String scope, final String entityCollectionName, final Object entityId, + final String relationCollectionName, final Object relationshipEntityId, Map params, Map headers) throws IOException + { + return get(rq, scope, 1, entityCollectionName, entityId, relationCollectionName, relationshipEntityId, params, headers); } public HttpResponse get(final RequestContext rq, final String scope, final int version, final String entityCollectionName, final Object entityId, - final String relationCollectionName, final Object relationshipEntityId, Map params) throws IOException + final String relationCollectionName, final Object relationshipEntityId, Map params, Map headers) throws IOException { + if (headers == null) + { + headers = Collections.emptyMap(); + } + RestApiEndpoint endpoint = new RestApiEndpoint(rq.getNetworkId(), scope, version, entityCollectionName, entityId, relationCollectionName, relationshipEntityId, params); String url = endpoint.getUrl(); GetMethod req = new GetMethod(url); + + for (Entry header : headers.entrySet()) + { + req.addRequestHeader(header.getKey(), header.getValue()); + } + return submitRequest(req, rq); } diff --git a/src/test/java/org/alfresco/rest/api/tests/client/data/Avatar.java b/src/test/java/org/alfresco/rest/api/tests/client/data/Avatar.java new file mode 100644 index 0000000000..b56aafb637 --- /dev/null +++ b/src/test/java/org/alfresco/rest/api/tests/client/data/Avatar.java @@ -0,0 +1,44 @@ +/* + * #%L + * Alfresco Remote API + * %% + * Copyright (C) 2005 - 2017 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ +package org.alfresco.rest.api.tests.client.data; + +import org.json.simple.JSONObject; + +import java.io.Serializable; + +public class Avatar implements Serializable, ExpectedComparison +{ + @Override + public void expected(Object other) + { + + } + + public static Avatar parseAvatar(JSONObject entry) + { + return new Avatar(); + } +}