/*
* #%L
* Alfresco Repository
* %%
* 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.repo.quickshare;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.alfresco.events.types.ActivityEvent;
import org.alfresco.events.types.Event;
import org.alfresco.model.ContentModel;
import org.alfresco.model.QuickShareModel;
import org.alfresco.repo.Client;
import org.alfresco.repo.Client.ClientType;
import org.alfresco.repo.action.executer.MailActionExecuter;
import org.alfresco.repo.admin.SysAdminParams;
import org.alfresco.repo.client.config.ClientAppConfig;
import org.alfresco.repo.client.config.ClientAppNotFoundException;
import org.alfresco.repo.copy.CopyBehaviourCallback;
import org.alfresco.repo.copy.CopyDetails;
import org.alfresco.repo.copy.CopyServicePolicies;
import org.alfresco.repo.copy.DoNothingCopyBehaviourCallback;
import org.alfresco.repo.events.EventPreparator;
import org.alfresco.repo.events.EventPublisher;
import org.alfresco.repo.node.NodeServicePolicies;
import org.alfresco.repo.policy.BehaviourFilter;
import org.alfresco.repo.policy.JavaBehaviour;
import org.alfresco.repo.policy.PolicyComponent;
import org.alfresco.repo.client.config.ClientAppConfig.ClientApp;
import org.alfresco.repo.security.authentication.AuthenticationUtil;
import org.alfresco.repo.security.authentication.AuthenticationUtil.RunAsWork;
import org.alfresco.repo.security.permissions.AccessDeniedException;
import org.alfresco.repo.site.SiteModel;
import org.alfresco.repo.tenant.TenantService;
import org.alfresco.repo.tenant.TenantUtil;
import org.alfresco.repo.tenant.TenantUtil.TenantRunAsWork;
import org.alfresco.repo.thumbnail.ThumbnailDefinition;
import org.alfresco.service.cmr.action.Action;
import org.alfresco.service.cmr.action.ActionService;
import org.alfresco.service.cmr.action.scheduled.ScheduledPersistedAction;
import org.alfresco.service.cmr.action.scheduled.ScheduledPersistedActionService;
import org.alfresco.service.cmr.attributes.AttributeService;
import org.alfresco.service.cmr.dictionary.DictionaryService;
import org.alfresco.service.cmr.quickshare.InvalidSharedIdException;
import org.alfresco.service.cmr.quickshare.QuickShareDTO;
import org.alfresco.service.cmr.quickshare.QuickShareDisabledException;
import org.alfresco.service.cmr.quickshare.QuickShareLinkExpiryAction;
import org.alfresco.service.cmr.quickshare.QuickShareLinkExpiryActionPersister;
import org.alfresco.service.cmr.quickshare.QuickShareService;
import org.alfresco.service.cmr.repository.ChildAssociationRef;
import org.alfresco.service.cmr.repository.ContentData;
import org.alfresco.service.cmr.repository.InvalidNodeRefException;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.cmr.search.ResultSet;
import org.alfresco.service.cmr.search.SearchParameters;
import org.alfresco.service.cmr.search.SearchService;
import org.alfresco.service.cmr.security.AccessStatus;
import org.alfresco.service.cmr.security.AuthorityService;
import org.alfresco.service.cmr.security.NoSuchPersonException;
import org.alfresco.service.cmr.security.PermissionService;
import org.alfresco.service.cmr.security.PersonService;
import org.alfresco.service.cmr.site.SiteService;
import org.alfresco.service.cmr.thumbnail.ThumbnailService;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.alfresco.util.EmailHelper;
import org.alfresco.util.EqualsHelper;
import org.alfresco.util.Pair;
import org.alfresco.util.ParameterCheck;
import org.alfresco.util.PropertyCheck;
import org.alfresco.util.UrlUtil;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.joda.time.DateTime;
import org.joda.time.Interval;
import org.joda.time.PeriodType;
import org.safehaus.uuid.UUID;
import org.safehaus.uuid.UUIDGenerator;
/**
* QuickShare Service implementation.
*
* In addition to the quick share service, this class also provides a BeforeDeleteNodePolicy and
* OnCopyNodePolicy for content with the QuickShare aspect.
*
* @author Alex Miller, janv, Jamal Kaabi-Mofrad
*/
public class QuickShareServiceImpl implements QuickShareService,
NodeServicePolicies.BeforeDeleteNodePolicy,
CopyServicePolicies.OnCopyNodePolicy,
NodeServicePolicies.OnRestoreNodePolicy
{
private static final Log logger = LogFactory.getLog(QuickShareServiceImpl.class);
static final String ATTR_KEY_SHAREDIDS_ROOT = ".sharedIds";
private static final String FTL_SHARED_NODE_URL = "shared_node_url";
private static final String FTL_SHARED_NODE_NAME = "shared_node_name";
private static final String FTL_SENDER_MESSAGE = "sender_message";
private static final String FTL_SENDER_FIRST_NAME = "sender_first_name";
private static final String FTL_SENDER_LAST_NAME = "sender_last_name";
private static final String FTL_TEMPLATE_ASSETS_URL = "template_assets_url";
private static final String CONFIG_SHARED_LINK_BASE_URL = "sharedLinkBaseUrl";
private static final String DEFAULT_EMAIL_SUBJECT = "quickshare.notifier.email.subject";
private static final String EMAIL_TEMPLATE_REF ="alfresco/templates/quickshare-email-templates/quickshare-email.default.template.ftl";
private AttributeService attributeService;
private DictionaryService dictionaryService;
private NodeService nodeService;
private PermissionService permissionService;
private PersonService personService;
private PolicyComponent policyComponent;
private TenantService tenantService;
private ThumbnailService thumbnailService;
private EventPublisher eventPublisher;
private ActionService actionService;
/** Component to determine which behaviours are active and which not */
private BehaviourFilter behaviourFilter;
private SearchService searchService;
private SiteService siteService;
private AuthorityService authorityService;
private SysAdminParams sysAdminParams;
private EmailHelper emailHelper;
private boolean enabled;
private String defaultEmailSender;
private ClientAppConfig clientAppConfig;
private ScheduledPersistedActionService scheduledPersistedActionService;
private QuickShareLinkExpiryActionPersister quickShareLinkExpiryActionPersister;
// The default period is in DAYS, but we allow HOURS|MINUTES as well for testing purposes.
private ExpiryDatePeriod expiryDatePeriod = ExpiryDatePeriod.DAYS;
/**
* Set the attribute service
*/
public void setAttributeService(AttributeService attributeService)
{
this.attributeService = attributeService;
}
/**
* Set the dictionary service
*/
public void setDictionaryService(DictionaryService dictionaryService)
{
this.dictionaryService = dictionaryService;
}
/**
* Set the node service
*/
public void setNodeService(NodeService nodeService)
{
this.nodeService = nodeService;
}
/**
* Set the Permission service
*/
public void setPermissionService(PermissionService permissionService)
{
this.permissionService = permissionService;
}
/**
* Set the person service
*/
public void setPersonService(PersonService personService)
{
this.personService = personService;
}
/**
* Set the policy component
*/
public void setPolicyComponent(PolicyComponent policyComponent)
{
this.policyComponent = policyComponent;
}
/**
* Set the tenant service
*/
public void setTenantService(TenantService tenantService)
{
this.tenantService = tenantService;
}
/**
* Set the thumbnail service
*/
public void setThumbnailService(ThumbnailService thumbnailService)
{
this.thumbnailService = thumbnailService;
}
/**
* Set the eventPublisher
*/
public void setEventPublisher(EventPublisher eventPublisher)
{
this.eventPublisher = eventPublisher;
}
/**
* Set the actionService
*/
public void setActionService(ActionService actionService)
{
this.actionService = actionService;
}
/**
* Spring configuration
*
* @param behaviourFilter the behaviourFilter to set
*/
public void setBehaviourFilter(BehaviourFilter behaviourFilter)
{
this.behaviourFilter = behaviourFilter;
}
/**
* Spring configuration
*
* @param searchService the searchService to set
*/
public void setSearchService(SearchService searchService)
{
this.searchService = searchService;
}
/**
* Spring configuration
*
* @param siteService the siteService to set
*/
public void setSiteService(SiteService siteService)
{
this.siteService = siteService;
}
/**
* Spring configuration
*
* @param authorityService the authorityService to set
*/
public void setAuthorityService(AuthorityService authorityService)
{
this.authorityService = authorityService;
}
/**
* Spring configuration
*
* @param sysAdminParams the sysAdminParams to set
*/
public void setSysAdminParams(SysAdminParams sysAdminParams)
{
this.sysAdminParams = sysAdminParams;
}
/**
* Spring configuration
*
* @param emailHelper the emailHelper to set
*/
public void setEmailHelper(EmailHelper emailHelper)
{
this.emailHelper = emailHelper;
}
/**
* Enable or disable this service.
*/
public void setEnabled(boolean enabled)
{
this.enabled = enabled;
}
/**
* Set the default email sender
*/
public void setDefaultEmailSender(String defaultEmailSender)
{
this.defaultEmailSender = defaultEmailSender;
}
/**
* Set the quickShare clientAppConfig
*/
public void setClientAppConfig(ClientAppConfig clientAppConfig)
{
this.clientAppConfig = clientAppConfig;
}
/**
* Spring configuration
*
* @param scheduledPersistedActionService the scheduledPersistedActionService to set
*/
public void setScheduledPersistedActionService(ScheduledPersistedActionService scheduledPersistedActionService)
{
this.scheduledPersistedActionService = scheduledPersistedActionService;
}
/**
* Spring configuration
*
* @param quickShareLinkExpiryActionPersister the quickShareLinkExpiryActionPersister to set
*/
public void setQuickShareLinkExpiryActionPersister(QuickShareLinkExpiryActionPersister quickShareLinkExpiryActionPersister)
{
this.quickShareLinkExpiryActionPersister = quickShareLinkExpiryActionPersister;
}
/**
* Spring configuration
*
* @param expiryDatePeriod the expiryDatePeriod to set
*/
public void setExpiryDatePeriod(String expiryDatePeriod)
{
if (expiryDatePeriod != null)
{
this.expiryDatePeriod = ExpiryDatePeriod.valueOf(expiryDatePeriod.toUpperCase());
}
}
private void checkMandatoryProperties()
{
PropertyCheck.mandatory(this, "attributeService", attributeService);
PropertyCheck.mandatory(this, "dictionaryService", dictionaryService);
PropertyCheck.mandatory(this, "nodeService", nodeService);
PropertyCheck.mandatory(this, "permissionService", permissionService);
PropertyCheck.mandatory(this, "personService", personService);
PropertyCheck.mandatory(this, "policyComponent", policyComponent);
PropertyCheck.mandatory(this, "tenantService", tenantService);
PropertyCheck.mandatory(this, "thumbnailService", thumbnailService);
PropertyCheck.mandatory(this, "eventPublisher", eventPublisher);
PropertyCheck.mandatory(this, "actionService", actionService);
PropertyCheck.mandatory(this, "behaviourFilter", behaviourFilter);
PropertyCheck.mandatory(this, "defaultEmailSender", defaultEmailSender);
PropertyCheck.mandatory(this, "clientAppConfig", clientAppConfig);
PropertyCheck.mandatory(this, "searchService", searchService);
PropertyCheck.mandatory(this, "siteService", siteService);
PropertyCheck.mandatory(this, "authorityService", authorityService);
PropertyCheck.mandatory(this, "sysAdminParams", sysAdminParams);
PropertyCheck.mandatory(this, "emailHelper", emailHelper);
PropertyCheck.mandatory(this, "scheduledPersistedActionService", scheduledPersistedActionService);
PropertyCheck.mandatory(this, "quickShareLinkExpiryActionPersister", quickShareLinkExpiryActionPersister);
}
/**
* The initialise method. Register our policies.
*/
public void init()
{
checkMandatoryProperties();
// Register interest in the beforeDeleteNode policy - note: currently for content only !!
policyComponent.bindClassBehaviour(
QName.createQName(NamespaceService.ALFRESCO_URI, "beforeDeleteNode"),
ContentModel.TYPE_CONTENT,
new JavaBehaviour(this, "beforeDeleteNode"));
//Register interest in the onCopyNodePolicy to block copying of quick share metadta
policyComponent.bindClassBehaviour(
CopyServicePolicies.OnCopyNodePolicy.QNAME,
QuickShareModel.ASPECT_QSHARE,
new JavaBehaviour(this, "getCopyCallback"));
this.policyComponent.bindClassBehaviour(
NodeServicePolicies.OnRestoreNodePolicy.QNAME,
QuickShareModel.ASPECT_QSHARE,
new JavaBehaviour(this, "onRestoreNode"));
}
@Override
public QuickShareDTO shareContent(final NodeRef nodeRef)
{
return shareContent(nodeRef, null);
}
@Override
public QuickShareDTO shareContent(NodeRef nodeRef, Date expiryDate) throws QuickShareDisabledException, InvalidNodeRefException
{
checkEnabled();
//Check the node is the correct type
final QName typeQName = nodeService.getType(nodeRef);
if (isSharable(typeQName) == false)
{
throw new InvalidNodeRefException(nodeRef);
}
final String sharedId;
// Only add the quick share aspect if it isn't already present.
// If it is retura dto built from the existing properties.
if (! nodeService.getAspects(nodeRef).contains(QuickShareModel.ASPECT_QSHARE))
{
UUID uuid = UUIDGenerator.getInstance().generateRandomBasedUUID();
sharedId = Base64.encodeBase64URLSafeString(uuid.toByteArray()); // => 22 chars (eg. q3bEKPeDQvmJYgt4hJxOjw)
final Map props = new HashMap(2);
props.put(QuickShareModel.PROP_QSHARE_SHAREDID, sharedId);
props.put(QuickShareModel.PROP_QSHARE_SHAREDBY, AuthenticationUtil.getRunAsUser());
// Disable audit to preserve modifier and modified date
// see MNT-11960
behaviourFilter.disableBehaviour(nodeRef, ContentModel.ASPECT_AUDITABLE);
try
{
// consumer/contributor should be able to add "shared" aspect (MNT-10366)
AuthenticationUtil.runAsSystem(new RunAsWork()
{
public Void doWork()
{
nodeService.addAspect(nodeRef, QuickShareModel.ASPECT_QSHARE, props);
return null;
}
});
}
finally
{
behaviourFilter.enableBehaviour(nodeRef, ContentModel.ASPECT_AUDITABLE);
}
final NodeRef tenantNodeRef = tenantService.getName(nodeRef);
TenantUtil.runAsDefaultTenant(new TenantRunAsWork()
{
public Void doWork() throws Exception
{
attributeService.setAttribute(tenantNodeRef, ATTR_KEY_SHAREDIDS_ROOT, sharedId);
return null;
}
});
final StringBuffer sb = new StringBuffer();
sb.append("{").append("\"sharedId\":\"").append(sharedId).append("\"").append("}");
eventPublisher.publishEvent(new EventPreparator(){
@Override
public Event prepareEvent(String user, String networkId, String transactionId)
{
return new ActivityEvent("quickshare", transactionId, networkId, user, nodeRef.getId(),
null, typeQName.toString(), Client.asType(ClientType.webclient), sb.toString(),
null, null, 0l, null);
}
});
if (logger.isInfoEnabled())
{
logger.info("QuickShare - shared content: "+sharedId+" ["+nodeRef+"]");
}
}
else
{
sharedId = (String)nodeService.getProperty(nodeRef, QuickShareModel.PROP_QSHARE_SHAREDID);
if (logger.isDebugEnabled())
{
logger.debug("QuickShare - content already shared: "+sharedId+" ["+nodeRef+"]");
}
}
if (expiryDate != null)
{
AuthenticationUtil.runAsSystem((RunAsWork) () -> {
// Create and save the expiry action
saveSharedLinkExpiryAction(sharedId, expiryDate);
// if we get here, it means the expiry date is validated and the action
// is created and saved, so now set the expiryDate property.
behaviourFilter.disableBehaviour(nodeRef, ContentModel.ASPECT_AUDITABLE);
try
{
nodeService.setProperty(nodeRef, QuickShareModel.PROP_QSHARE_EXPIRY_DATE, expiryDate);
}
finally
{
behaviourFilter.enableBehaviour(nodeRef, ContentModel.ASPECT_AUDITABLE);
}
return null;
});
}
return new QuickShareDTO(sharedId, expiryDate);
}
/**
* Is this service enable?
* @throws QuickShareDisabledException if it isn't.
*/
private void checkEnabled()
{
if (enabled == false)
{
throw new QuickShareDisabledException("QuickShare is disabled system-wide");
}
}
@SuppressWarnings("unchecked")
@Override
public Map getMetaData(NodeRef nodeRef)
{
// TODO This functionality MUST be available when quickshare is also disabled, therefor refactor it out from the quickshare package to a more common package.
Map nodeProps = nodeService.getProperties(nodeRef);
ContentData contentData = (ContentData)nodeService.getProperty(nodeRef, ContentModel.PROP_CONTENT);
String modifierUserName = (String)nodeProps.get(ContentModel.PROP_MODIFIER);
Map personProps = null;
if (modifierUserName != null)
{
try
{
NodeRef personRef = personService.getPerson(modifierUserName);
if (personRef != null)
{
personProps = nodeService.getProperties(personRef);
}
}
catch (NoSuchPersonException nspe)
{
// absorb this exception - eg. System (or maybe the user has been deleted)
if (logger.isInfoEnabled())
{
logger.info("MetaDataGet - no such person: "+modifierUserName);
}
}
}
Map metadata = new HashMap(8);
metadata.put("nodeRef", nodeRef.toString());
metadata.put("name", nodeProps.get(ContentModel.PROP_NAME));
metadata.put("title", nodeProps.get(ContentModel.PROP_TITLE));
if (contentData != null)
{
metadata.put("mimetype", contentData.getMimetype());
metadata.put("size", contentData.getSize());
}
else
{
metadata.put("size", 0L);
}
metadata.put("modified", nodeProps.get(ContentModel.PROP_MODIFIED));
if (personProps != null)
{
metadata.put("modifierFirstName", personProps.get(ContentModel.PROP_FIRSTNAME));
metadata.put("modifierLastName", personProps.get(ContentModel.PROP_LASTNAME));
}
// thumbnail defs for this nodeRef
List thumbnailDefs = new ArrayList(7);
if (contentData != null)
{
// Note: thumbnail defs only appear in this list if they can produce a thumbnail for the content
// found in the content property of this node. This will be determined by looking at the mimetype of the content
// and the destination mimetype of the thumbnail.
List thumbnailDefinitions = thumbnailService.getThumbnailRegistry().getThumbnailDefinitions(contentData.getMimetype(), contentData.getSize());
for (ThumbnailDefinition thumbnailDefinition : thumbnailDefinitions)
{
thumbnailDefs.add(thumbnailDefinition.getName());
}
}
metadata.put("thumbnailDefinitions", thumbnailDefs);
// thumbnail instances for this nodeRef
List thumbnailRefs = thumbnailService.getThumbnails(nodeRef, ContentModel.PROP_CONTENT, null, null);
List thumbnailNames = new ArrayList(thumbnailRefs.size());
for (NodeRef thumbnailRef : thumbnailRefs)
{
thumbnailNames.add((String)nodeService.getProperty(thumbnailRef, ContentModel.PROP_NAME));
}
metadata.put("thumbnailNames", thumbnailNames);
metadata.put("lastThumbnailModificationData", (List)nodeProps.get(ContentModel.PROP_LAST_THUMBNAIL_MODIFICATION_DATA));
if (nodeProps.containsKey(QuickShareModel.PROP_QSHARE_SHAREDID))
{
metadata.put("sharedId", nodeProps.get(QuickShareModel.PROP_QSHARE_SHAREDID));
metadata.put("expiryDate", nodeProps.get(QuickShareModel.PROP_QSHARE_EXPIRY_DATE));
}
else
{
QName type = nodeService.getType(nodeRef);
boolean sharable = isSharable(type);
metadata.put("sharable", sharable);
}
Map model = new HashMap(2);
model.put("item", metadata);
return model;
}
@Override
public Pair getTenantNodeRefFromSharedId(final String sharedId)
{
NodeRef nodeRef = TenantUtil.runAsDefaultTenant(new TenantRunAsWork()
{
public NodeRef doWork() throws Exception
{
return (NodeRef) attributeService.getAttribute(ATTR_KEY_SHAREDIDS_ROOT, sharedId);
}
});
if (nodeRef == null)
{
/* TODO
* Temporary fix for RA-1093 and MNT-16224. The extra lookup should be
* removed (the same as before, just throw the 'InvalidSharedIdException' exception) when we
* have a system wide patch to remove the 'shared' aspect of the nodes that have been archived while shared.
*/
// TMDQ
final String query = "+TYPE:\"cm:content\" AND +ASPECT:\"qshare:shared\" AND =qshare:sharedId:\"" + sharedId + "\"";
SearchParameters sp = new SearchParameters();
sp.setLanguage(SearchService.LANGUAGE_FTS_ALFRESCO);
sp.setQuery(query);
sp.addStore(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE);
List nodeRefs = null;
ResultSet results = null;
try
{
results = searchService.query(sp);
nodeRefs = results.getNodeRefs();
}
catch (Exception ex)
{
throw new InvalidSharedIdException(sharedId);
}
finally
{
if (results != null)
{
results.close();
}
}
if (nodeRefs.size() != 1)
{
throw new InvalidSharedIdException(sharedId);
}
nodeRef = tenantService.getName(nodeRefs.get(0));
}
// note: relies on tenant-specific (ie. mangled) nodeRef
String tenantDomain = tenantService.getDomain(nodeRef.getStoreRef().getIdentifier());
return new Pair<>(tenantDomain, tenantService.getBaseName(nodeRef));
}
@Override
public Map getMetaData(String sharedId)
{
checkEnabled();
Pair pair = getTenantNodeRefFromSharedId(sharedId);
final String tenantDomain = pair.getFirst();
final NodeRef nodeRef = pair.getSecond();
Map model = TenantUtil.runAsSystemTenant(new TenantRunAsWork