diff --git a/config/alfresco/core-services-context.xml b/config/alfresco/core-services-context.xml index 53b7a6de92..60ad2dcf49 100644 --- a/config/alfresco/core-services-context.xml +++ b/config/alfresco/core-services-context.xml @@ -472,6 +472,7 @@ alfresco.messages.jbpm-engine-messages alfresco.messages.activiti-engine-messages alfresco.messages.wdr-messages + alfresco.messages.subscription-service diff --git a/config/alfresco/import-export-context.xml b/config/alfresco/import-export-context.xml index 5b40577104..befb9626de 100644 --- a/config/alfresco/import-export-context.xml +++ b/config/alfresco/import-export-context.xml @@ -525,6 +525,11 @@ alfresco/templates/activities-email-templates.acp alfresco/messages/bootstrap-spaces + + /${spaces.company_home.childname}/${spaces.dictionary.childname}/${spaces.templates.email.childname} + alfresco/templates/following-email-templates.acp + alfresco/messages/bootstrap-spaces + /${spaces.company_home.childname}/${spaces.dictionary.childname}/${spaces.templates.rss.childname} alfresco/templates/rss_templates.acp diff --git a/config/alfresco/messages/bootstrap-spaces.properties b/config/alfresco/messages/bootstrap-spaces.properties index b2a5d56714..a6af51e5a5 100644 --- a/config/alfresco/messages/bootstrap-spaces.properties +++ b/config/alfresco/messages/bootstrap-spaces.properties @@ -97,6 +97,11 @@ email.template.email_template_for_notifying_users_of_an_Invite=Email template fo email.templates.email_template_for_notifying_new_users=Email template used to inform new users of their accounts +spaces.templates.email.following.name=Following Email Templates +spaces.templates.email.following.description=Following Email Templates + +email.templates.email_template_for_following_notifications=Email template used to generate following notification emails + version.default=Default version version.french=French version version.german=German version diff --git a/config/alfresco/messages/patch-service.properties b/config/alfresco/messages/patch-service.properties index f33888d2b7..f4b97d624d 100644 --- a/config/alfresco/messages/patch-service.properties +++ b/config/alfresco/messages/patch-service.properties @@ -409,5 +409,13 @@ patch.exampleJavaScript.description=Loads sample Javascript file into datadictio patch.fixAclInheritance.description=Fixes any ACL inheritance issues. patch.fixAclInheritance.result=Fixed {0} ACLs. +patch.followingMailTemplates.description=Adds email templates for following notifications + +patch.activitiesTemplatesUpdate.description=Updates activities email templates. +patch.activitiesTemplatesUpdate.err.template_folder_not_found=Activities email template folder could not be found. +patch.activitiesTemplatesUpdate.err.source_not_found=New activities email template ACP could not be found. +patch.activitiesTemplatesUpdate.err.update_failed=Could not update activities email template: {0} +patch.activitiesTemplatesUpdate.result=Updated {0} activities email templates. + patch.avmToAdmRemoteStore.description=Migrates Share Surf config from AVM sitestore to DM Sites folder. patch.avmToAdmRemoteStore.complete=Completed Share Surf config migration. diff --git a/config/alfresco/messages/subscription-service.properties b/config/alfresco/messages/subscription-service.properties new file mode 100644 index 0000000000..fa480dc1ed --- /dev/null +++ b/config/alfresco/messages/subscription-service.properties @@ -0,0 +1,7 @@ +# Subscription service messages + +subscription.notification.email.subject={0} is now following you + +subscription_service.err.disabled=The subscription is disabled +subscription_service.err.write-denied=No permissions to update +subscription_service.err.private-list=This list is marked as private \ No newline at end of file diff --git a/config/alfresco/patch/patch-services-context.xml b/config/alfresco/patch/patch-services-context.xml index 6ac042d986..8f35441964 100644 --- a/config/alfresco/patch/patch-services-context.xml +++ b/config/alfresco/patch/patch-services-context.xml @@ -2600,6 +2600,50 @@ + + + + patch.activitiesTemplatesUpdate + + + patch.activitiesTemplatesUpdate.description + + + + + + + + + + + + + + + + + alfresco/templates/activities-email-templates.acp + + + + + patch.followingMailTemplates + patch.followingMailTemplates.description + 0 + 5010 + 5011 + + + + + + /${spaces.company_home.childname}/${spaces.dictionary.childname}/${spaces.templates.email.childname} + alfresco/templates/following-email-templates.acp + alfresco/messages/bootstrap-spaces + + + patch.newUserEmailTemplates diff --git a/config/alfresco/repository.properties b/config/alfresco/repository.properties index 9f8a7d3a42..02ce8c2d86 100644 --- a/config/alfresco/repository.properties +++ b/config/alfresco/repository.properties @@ -376,6 +376,7 @@ spaces.templates.content.childname=app:content_templates spaces.templates.email.childname=app:email_templates spaces.templates.email.invite1.childname=app:invite_email_templates spaces.templates.email.notify.childname=app:notify_email_templates +spaces.templates.email.following.childname=app:following spaces.templates.rss.childname=app:rss_templates spaces.savedsearches.childname=app:saved_searches spaces.scripts.childname=app:scripts diff --git a/config/alfresco/subscription-service-context.xml b/config/alfresco/subscription-service-context.xml index c04e5888ca..71f9d3ed58 100644 --- a/config/alfresco/subscription-service-context.xml +++ b/config/alfresco/subscription-service-context.xml @@ -25,6 +25,10 @@ + + + + \ No newline at end of file diff --git a/config/alfresco/templates/activities-email-templates.acp b/config/alfresco/templates/activities-email-templates.acp index de1f1a4bbb..54292f0086 100644 Binary files a/config/alfresco/templates/activities-email-templates.acp and b/config/alfresco/templates/activities-email-templates.acp differ diff --git a/config/alfresco/templates/following-email-templates.acp b/config/alfresco/templates/following-email-templates.acp new file mode 100644 index 0000000000..a7ab3083df Binary files /dev/null and b/config/alfresco/templates/following-email-templates.acp differ diff --git a/source/java/org/alfresco/repo/admin/patch/impl/ActivitiesTemplatesUpdatePatch.java b/source/java/org/alfresco/repo/admin/patch/impl/ActivitiesTemplatesUpdatePatch.java new file mode 100644 index 0000000000..bc17faab71 --- /dev/null +++ b/source/java/org/alfresco/repo/admin/patch/impl/ActivitiesTemplatesUpdatePatch.java @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2005-2011 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * 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 . + */ +package org.alfresco.repo.admin.patch.impl; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.Serializable; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.model.ContentModel; +import org.alfresco.repo.admin.patch.AbstractPatch; +import org.alfresco.repo.version.VersionModel; +import org.alfresco.service.cmr.admin.PatchException; +import org.alfresco.service.cmr.model.FileFolderService; +import org.alfresco.service.cmr.repository.ContentWriter; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.repository.StoreRef; +import org.alfresco.service.cmr.version.VersionService; +import org.alfresco.service.cmr.version.VersionType; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.TempFileProvider; +import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; +import org.apache.commons.compress.archivers.zip.ZipFile; +import org.springframework.extensions.surf.util.I18NUtil; +import org.springframework.util.FileCopyUtils; + +/** + * Patch to update the activities email templates. Current templates become + * versions of the new templates. + * + * @author Florian Mueller + */ +public class ActivitiesTemplatesUpdatePatch extends AbstractPatch +{ + private static final String MSG_SUCCESS = "patch.activitiesTemplatesUpdate.result"; + + protected FileFolderService fileFolderService; + protected VersionService versionService; + + protected String newTemplatesFile; + protected String newTemplatesName; + + protected ZipFile zipFile; + + public void setFileFolderService(FileFolderService fileFolderService) + { + this.fileFolderService = fileFolderService; + } + + public void setVersionService(VersionService versionService) + { + this.versionService = versionService; + } + + public void setNewTemplatesFile(String newTemplatesFile) + { + this.newTemplatesFile = newTemplatesFile; + + int x = newTemplatesFile.lastIndexOf("/"); + if (x < 0) + { + newTemplatesName = newTemplatesFile; + } else + { + newTemplatesName = newTemplatesFile.substring(x + 1); + } + + x = newTemplatesName.lastIndexOf("."); + if (x > 0) + { + newTemplatesName = newTemplatesName.substring(0, x); + } + } + + @Override + protected String applyInternal() throws Exception + { + // get templates folder + NodeRef templateFolder = getTemplateFolder(); + + // get ACP file + ZipFile zipFile = getZipFile(); + + // iterate over all templates in the ACP file and apply version + @SuppressWarnings("unchecked") + Enumeration zae = (Enumeration) zipFile.getEntries(); + int count = 0; + while (zae.hasMoreElements()) + { + ZipArchiveEntry entry = zae.nextElement(); + if (!(entry.getName().startsWith(newTemplatesName + "/") && entry.getName().endsWith(".ftl"))) + { + // ignore non-template files + continue; + } + + // find matching node and add version + NodeRef nodeRef = findMatchingNode(templateFolder, entry); + if (nodeRef != null) + { + addNewVersion(nodeRef, zipFile, entry); + count++; + } + } + + return I18NUtil.getMessage(MSG_SUCCESS, count); + } + + protected NodeRef getTemplateFolder() + { + String xpath = "app:company_home/app:dictionary/app:email_templates/cm:activities"; + try + { + NodeRef rootNodeRef = nodeService.getRootNode(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE); + List nodeRefs = searchService.selectNodes(rootNodeRef, xpath, null, namespaceService, false); + + if (nodeRefs.size() < 1) + { + throw new PatchException("patch.activitiesTemplatesUpdate.err.template_folder_not_found"); + } + + return nodeRefs.get(0); + } catch (Exception e) + { + throw new PatchException("patch.activitiesTemplatesUpdate.err.template_folder_not_found", e); + } + } + + protected ZipFile getZipFile() + { + InputStream templateFileStream = ActivitiesTemplatesUpdatePatch.class.getClassLoader().getResourceAsStream( + newTemplatesFile); + if (templateFileStream == null) + { + throw new PatchException("patch.activitiesTemplatesUpdate.err.source_not_found"); + } + + try + { + File tempFile = TempFileProvider.createTempFile("templateFile", ".tmp"); + FileOutputStream os = new FileOutputStream(tempFile); + FileCopyUtils.copy(templateFileStream, os); + + return new ZipFile(tempFile, "Cp437"); + } catch (IOException e) + { + throw new PatchException("patch.activitiesTemplatesUpdate.err.source_not_found", e); + } + } + + protected NodeRef findMatchingNode(NodeRef parentNodeRef, ZipArchiveEntry entry) + { + String name = entry.getName(); + int x = name.lastIndexOf("/"); + if (x > 0) + { + name = name.substring(x + 1); + } + + return nodeService.getChildByName(parentNodeRef, ContentModel.ASSOC_CONTAINS, name); + } + + protected void addNewVersion(NodeRef nodeRef, ZipFile zipFile, ZipArchiveEntry entry) + { + try + { + if (nodeService.hasAspect(nodeRef, ContentModel.ASPECT_VERSIONABLE) == false) + { + Map props = new HashMap(); + props.put(ContentModel.PROP_INITIAL_VERSION, true); + props.put(ContentModel.PROP_AUTO_VERSION, false); + nodeService.addAspect(nodeRef, ContentModel.ASPECT_VERSIONABLE, props); + } + + Map versionProperties = new HashMap(); + versionProperties.put(VersionModel.PROP_VERSION_TYPE, VersionType.MAJOR); + + versionService.createVersion(nodeRef, versionProperties); + + ContentWriter writer = fileFolderService.getWriter(nodeRef); + writer.setMimetype("text/plain"); + writer.setEncoding("UTF-8"); + writer.putContent(zipFile.getInputStream(entry)); + } catch (Exception e) + { + throw new PatchException("patch.activitiesTemplatesUpdate.err.update_failed", e); + } + } +} diff --git a/source/java/org/alfresco/repo/subscriptions/SubscriptionServiceImpl.java b/source/java/org/alfresco/repo/subscriptions/SubscriptionServiceImpl.java index 6d685fa414..fbd57f2fcc 100644 --- a/source/java/org/alfresco/repo/subscriptions/SubscriptionServiceImpl.java +++ b/source/java/org/alfresco/repo/subscriptions/SubscriptionServiceImpl.java @@ -19,16 +19,27 @@ package org.alfresco.repo.subscriptions; import java.io.Serializable; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import org.alfresco.model.ContentModel; import org.alfresco.query.PagingRequest; +import org.alfresco.repo.action.executer.MailActionExecuter; import org.alfresco.repo.activities.ActivityType; import org.alfresco.repo.domain.subscriptions.SubscriptionsDAO; +import org.alfresco.repo.search.SearcherException; import org.alfresco.repo.security.authentication.AuthenticationUtil; import org.alfresco.repo.security.permissions.AccessDeniedException; +import org.alfresco.service.cmr.action.Action; +import org.alfresco.service.cmr.action.ActionService; import org.alfresco.service.cmr.activities.ActivityService; +import org.alfresco.service.cmr.model.FileFolderService; 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.SearchService; import org.alfresco.service.cmr.security.AuthorityService; import org.alfresco.service.cmr.security.PersonService; import org.alfresco.service.cmr.subscriptions.PagingFollowingResults; @@ -37,10 +48,12 @@ import org.alfresco.service.cmr.subscriptions.PrivateSubscriptionListException; import org.alfresco.service.cmr.subscriptions.SubscriptionItemTypeEnum; import org.alfresco.service.cmr.subscriptions.SubscriptionService; import org.alfresco.service.cmr.subscriptions.SubscriptionsDisabledException; +import org.alfresco.service.namespace.NamespaceService; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.json.JSONException; import org.json.JSONObject; +import org.springframework.extensions.surf.util.I18NUtil; public class SubscriptionServiceImpl implements SubscriptionService { @@ -54,9 +67,12 @@ public class SubscriptionServiceImpl implements SubscriptionService private static final String FOLLOWER_FIRSTNAME = "followerFirstName"; private static final String FOLLOWER_LASTNAME = "followerLastName"; private static final String FOLLOWER_USERNAME = "followerUserName"; + private static final String FOLLOWER_JOBTITLE = "followerJobTitle"; private static final String USER_FIRSTNAME = "userFirstName"; private static final String USER_LASTNAME = "userLastName"; private static final String USER_USERNAME = "userUserName"; + private static final String FOLLOWING_COUNT = "followingCount"; + private static final String FOLLOWER_COUNT = "followerCount"; private static final String SUBSCRIBER_FIRSTNAME = "subscriberFirstName"; private static final String SUBSCRIBER_LASTNAME = "subscriberLastName"; @@ -68,6 +84,10 @@ public class SubscriptionServiceImpl implements SubscriptionService protected PersonService personService; protected ActivityService activityService; protected AuthorityService authorityService; + protected ActionService actionService; + protected SearchService searchService; + protected NamespaceService namespaceService; + protected FileFolderService fileFolderService; /** * Sets the subscriptions DAO. @@ -109,6 +129,38 @@ public class SubscriptionServiceImpl implements SubscriptionService this.authorityService = authorityService; } + /** + * Sets the action service. + */ + public final void setActionService(ActionService actionService) + { + this.actionService = actionService; + } + + /** + * Set the search service. + */ + public final void setSearchService(SearchService searchService) + { + this.searchService = searchService; + } + + /** + * Set the namespace service. + */ + public final void setNamespaceService(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + /** + * Set the fileFolder service. + */ + public final void setFileFolderService(FileFolderService fileFolderService) + { + this.fileFolderService = fileFolderService; + } + @Override public PagingSubscriptionResults getSubscriptions(String userId, SubscriptionItemTypeEnum type, PagingRequest pagingRequest) @@ -215,9 +267,10 @@ public class SubscriptionServiceImpl implements SubscriptionService if (userId.equalsIgnoreCase(AuthenticationUtil.getRunAsUser())) { - String activityDataJSON = null; try { + String activityDataJSON = null; + NodeRef followerNode = personService.getPerson(userId, false); NodeRef userNode = personService.getPerson(userToFollow, false); JSONObject activityData = new JSONObject(); @@ -229,13 +282,23 @@ public class SubscriptionServiceImpl implements SubscriptionService activityData.put(USER_FIRSTNAME, nodeService.getProperty(userNode, ContentModel.PROP_FIRSTNAME)); activityData.put(USER_LASTNAME, nodeService.getProperty(userNode, ContentModel.PROP_LASTNAME)); activityDataJSON = activityData.toString(); + + activityService.postActivity(ActivityType.SUBSCRIPTIONS_FOLLOW, null, ACTIVITY_TOOL, activityDataJSON); + } catch (JSONException je) { // log error, subsume exception logger.error("Failed to get activity data: " + je); } - activityService.postActivity(ActivityType.SUBSCRIPTIONS_FOLLOW, null, ACTIVITY_TOOL, activityDataJSON); + try + { + sendFollowingMail(userId, userToFollow); + } catch (Exception e) + { + // log error, subsume exception + logger.error("Failed to send following email: " + e); + } } } @@ -379,4 +442,105 @@ public class SubscriptionServiceImpl implements SubscriptionService throw new IllegalArgumentException("Only user nodes supported!"); } } + + /** + * Sends an email to the person that is followed. + */ + protected void sendFollowingMail(String userId, String userToFollow) + { + NodeRef followerNode = personService.getPerson(userId, false); + NodeRef userNode = personService.getPerson(userToFollow, false); + + Serializable emailFeedDisabled = nodeService.getProperty(userNode, ContentModel.PROP_EMAIL_FEED_DISABLED); + if (emailFeedDisabled instanceof Boolean && ((Boolean) emailFeedDisabled).booleanValue()) + { + // this user doesn't want to be notified + return; + } + + Serializable emailAddress = nodeService.getProperty(userNode, ContentModel.PROP_EMAIL); + if (emailAddress == null) + { + // we can't send an email without email address + return; + } + + NodeRef templateNodeRef = getEmailTemplateRef(); + if (templateNodeRef == null) + { + // we can't send an email without template + return; + } + + // compile the mail subject + String followerFullName = (nodeService.getProperty(followerNode, ContentModel.PROP_FIRSTNAME) + " " + nodeService + .getProperty(followerNode, ContentModel.PROP_LASTNAME)).trim(); + String subjectText = I18NUtil.getMessage("subscription.notification.email.subject", followerFullName); + + Map model = new HashMap(); + + model.put(FOLLOWER_USERNAME, userId); + model.put(FOLLOWER_FIRSTNAME, nodeService.getProperty(followerNode, ContentModel.PROP_FIRSTNAME)); + model.put(FOLLOWER_LASTNAME, nodeService.getProperty(followerNode, ContentModel.PROP_LASTNAME)); + model.put(FOLLOWER_JOBTITLE, nodeService.getProperty(followerNode, ContentModel.PROP_JOBTITLE)); + model.put(USER_USERNAME, userToFollow); + model.put(USER_FIRSTNAME, nodeService.getProperty(userNode, ContentModel.PROP_FIRSTNAME)); + model.put(USER_LASTNAME, nodeService.getProperty(userNode, ContentModel.PROP_LASTNAME)); + model.put(FOLLOWING_COUNT, -1); + try + { + model.put(FOLLOWING_COUNT, getFollowingCount(userId)); + } catch (Exception e) + { + } + model.put(FOLLOWER_COUNT, -1); + try + { + model.put(FOLLOWER_COUNT, getFollowersCount(userId)); + } catch (Exception e) + { + } + + Action mail = actionService.createAction(MailActionExecuter.NAME); + + mail.setParameterValue(MailActionExecuter.PARAM_TO, emailAddress); + mail.setParameterValue(MailActionExecuter.PARAM_SUBJECT, subjectText); + mail.setParameterValue(MailActionExecuter.PARAM_TEMPLATE, templateNodeRef); + mail.setParameterValue(MailActionExecuter.PARAM_TEMPLATE_MODEL, (Serializable) model); + + actionService.executeAction(mail, null); + } + + /** + * Returns the NodeRef of the email template or null if the + * template coudln't be found. + */ + protected NodeRef getEmailTemplateRef() + { + // Find the following email template + String xpath = "app:company_home/app:dictionary/app:email_templates/app:following/cm:following-email.html.ftl"; + try + { + NodeRef rootNodeRef = nodeService.getRootNode(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE); + List nodeRefs = searchService.selectNodes(rootNodeRef, xpath, null, namespaceService, false); + if (nodeRefs.size() > 1) + { + logger.error("Found too many email templates using: " + xpath); + nodeRefs = Collections.singletonList(nodeRefs.get(0)); + } else if (nodeRefs.size() == 0) + { + logger.error("Cannot find the email template using " + xpath); + return null; + } + // Now localise this + NodeRef base = nodeRefs.get(0); + NodeRef local = fileFolderService.getLocalizedSibling(base); + return local; + } catch (SearcherException e) + { + logger.error("Cannot find the email template!", e); + } + + return null; + } }