Java tutorial
/* * Copyright (C) 2003-2016 eXo Platform SAS. * * This 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 2.1 of * the License, or (at your option) any later version. * * This software 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 this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.exoplatform.outlook; import com.ibm.icu.text.Transliterator; import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang.StringEscapeUtils; import org.apache.http.entity.ContentType; import org.exoplatform.commons.utils.ISO8601; import org.exoplatform.commons.utils.ListAccess; import org.exoplatform.container.ExoContainerContext; import org.exoplatform.container.configuration.ConfigurationException; import org.exoplatform.container.xml.InitParams; import org.exoplatform.forum.bbcode.core.ExtendedBBCodeProvider; import org.exoplatform.forum.common.CommonUtils; import org.exoplatform.forum.common.TransformHTML; import org.exoplatform.forum.common.webui.WebUIUtils; import org.exoplatform.forum.ext.activity.BuildLinkUtils; import org.exoplatform.forum.ext.activity.BuildLinkUtils.PORTLET_INFO; import org.exoplatform.forum.service.Category; import org.exoplatform.forum.service.Forum; import org.exoplatform.forum.service.ForumAdministration; import org.exoplatform.forum.service.ForumService; import org.exoplatform.forum.service.ForumServiceUtils; import org.exoplatform.forum.service.MessageBuilder; import org.exoplatform.forum.service.Topic; import org.exoplatform.outlook.forum.ForumUtils; import org.exoplatform.outlook.jcr.File; import org.exoplatform.outlook.jcr.Folder; import org.exoplatform.outlook.jcr.HierarchyNode; import org.exoplatform.outlook.jcr.NodeFinder; import org.exoplatform.outlook.jcr.UserDocuments; import org.exoplatform.outlook.mail.MailAPI; import org.exoplatform.outlook.mail.MailServerException; import org.exoplatform.outlook.social.OutlookAttachmentActivity; import org.exoplatform.outlook.social.OutlookMessageActivity; import org.exoplatform.portal.application.PortalRequestContext; import org.exoplatform.portal.config.model.PortalConfig; import org.exoplatform.portal.mop.SiteType; import org.exoplatform.portal.webui.util.Util; import org.exoplatform.services.cms.BasePath; import org.exoplatform.services.cms.documents.TrashService; import org.exoplatform.services.cms.drives.DriveData; import org.exoplatform.services.cms.drives.ManageDriveService; import org.exoplatform.services.jcr.RepositoryService; import org.exoplatform.services.jcr.access.AccessControlList; import org.exoplatform.services.jcr.access.PermissionType; import org.exoplatform.services.jcr.core.ExtendedNode; import org.exoplatform.services.jcr.ext.app.SessionProviderService; import org.exoplatform.services.jcr.ext.common.SessionProvider; import org.exoplatform.services.jcr.ext.hierarchy.NodeHierarchyCreator; import org.exoplatform.services.listener.ListenerService; import org.exoplatform.services.log.ExoLogger; import org.exoplatform.services.log.Log; import org.exoplatform.services.organization.Group; import org.exoplatform.services.organization.Membership; import org.exoplatform.services.organization.MembershipType; import org.exoplatform.services.organization.OrganizationService; import org.exoplatform.services.resources.ResourceBundleService; import org.exoplatform.services.security.ConversationState; import org.exoplatform.services.security.IdentityConstants; import org.exoplatform.services.wcm.core.NodetypeConstant; import org.exoplatform.social.core.activity.model.ExoSocialActivity; import org.exoplatform.social.core.activity.model.ExoSocialActivityImpl; import org.exoplatform.social.core.application.PeopleService; import org.exoplatform.social.core.application.SpaceActivityPublisher; import org.exoplatform.social.core.identity.model.Identity; import org.exoplatform.social.core.identity.provider.OrganizationIdentityProvider; import org.exoplatform.social.core.identity.provider.SpaceIdentityProvider; import org.exoplatform.social.core.manager.ActivityManager; import org.exoplatform.social.core.manager.IdentityManager; import org.exoplatform.social.core.service.LinkProvider; import org.exoplatform.social.core.space.SpaceUtils; import org.exoplatform.social.core.space.model.Space; import org.exoplatform.social.core.space.spi.SpaceService; import org.exoplatform.social.webui.activity.UIDefaultActivity; import org.exoplatform.wcm.webui.reader.ContentReader; import org.exoplatform.web.url.navigation.NavigationResource; import org.exoplatform.web.url.navigation.NodeURL; import org.exoplatform.webui.application.WebuiRequestContext; import org.exoplatform.wiki.WikiException; import org.exoplatform.wiki.mow.api.Page; import org.exoplatform.wiki.mow.api.Permission; import org.exoplatform.wiki.mow.api.PermissionEntry; import org.exoplatform.wiki.mow.api.Wiki; import org.exoplatform.wiki.rendering.RenderingService; import org.exoplatform.wiki.resolver.TitleResolver; import org.exoplatform.wiki.service.IDType; import org.exoplatform.wiki.service.WikiService; import org.exoplatform.ws.frameworks.json.value.JsonValue; import org.owasp.html.HtmlPolicyBuilder; import org.owasp.html.PolicyFactory; import org.owasp.html.Sanitizers; import org.picocontainer.Startable; import org.xwiki.rendering.syntax.Syntax; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.security.AccessControlException; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Random; import java.util.ResourceBundle; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Predicate; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.jcr.AccessDeniedException; import javax.jcr.InvalidItemStateException; import javax.jcr.Item; import javax.jcr.Node; import javax.jcr.NodeIterator; import javax.jcr.PathNotFoundException; import javax.jcr.Property; import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.jcr.ValueFormatException; import javax.jcr.nodetype.ConstraintViolationException; import javax.jcr.query.Query; import javax.jcr.query.QueryManager; import javax.jcr.query.QueryResult; import javax.servlet.http.HttpServletRequest; /** * Service implementing {@link OutlookService} and {@link Startable}.<br> * Created by The eXo Platform SAS. * * @author <a href="mailto:pnedonosko@exoplatform.com">Peter Nedonosko</a> * @version $Id: AnaplanServiceImpl.java 00000 Mar 3, 2016 pnedonosko $ */ public class OutlookServiceImpl implements OutlookService, Startable { /** The Constant MAILSERVER_URL. */ public static final String MAILSERVER_URL = "mailserver-url"; /** The Constant EXO_PRIVILEGEABLE. */ protected static final String EXO_PRIVILEGEABLE = "exo:privilegeable"; /** The Constant OUTLOOK_MESSAGES_TITLE. */ protected static final String OUTLOOK_MESSAGES_TITLE = "Outlook Messages"; /** The Constant OUTLOOK_MESSAGES_NAME. */ protected static final String OUTLOOK_MESSAGES_NAME = "outlook-messages"; /** The Constant WIKI_PERMISSION_ANY. */ protected static final String WIKI_PERMISSION_ANY = "any"; /** The Constant UPLAODS_FOLDER_TITLE. */ protected static final String UPLAODS_FOLDER_TITLE = "Uploads"; /** The Constant SPACES_HOME. */ protected static final String SPACES_HOME = "/Groups/spaces"; /** The Constant ROOT_USER. */ protected static final String ROOT_USER = "root"; /** The Constant PERSONAL_DOCUMENTS. */ protected static final String PERSONAL_DOCUMENTS = "Personal Documents"; /** The Constant READER_PERMISSION. */ protected static final String[] READER_PERMISSION = new String[] { PermissionType.READ }; /** The Constant MANAGER_PERMISSION. */ protected static final String[] MANAGER_PERMISSION = new String[] { PermissionType.READ, PermissionType.REMOVE }; /** The Constant LOG. */ protected static final Log LOG = ExoLogger.getLogger(OutlookServiceImpl.class); /** The Constant RANDOM. */ protected static final Random RANDOM = new Random(); /** The Constant accentsConverter. */ protected static final Transliterator accentsConverter = Transliterator .getInstance("Latin; NFD; [:Nonspacing Mark:] Remove; NFC;"); /** * The Class UserFolder. */ protected class UserFolder extends Folder { /** * Instantiates a new user folder. * * @param parent the parent * @param node the node * @throws RepositoryException the repository exception * @throws OutlookException the outlook exception */ protected UserFolder(Folder parent, Node node) throws RepositoryException, OutlookException { super(parent, node); } /** * Instantiates a new user folder. * * @param parentPath the parent path * @param node the node * @throws RepositoryException the repository exception * @throws OutlookException the outlook exception */ protected UserFolder(String parentPath, Node node) throws RepositoryException, OutlookException { super(parentPath, node); } /** * Instantiates a new user folder. * * @param node the node * @throws RepositoryException the repository exception * @throws OutlookException the outlook exception */ protected UserFolder(Node node) throws RepositoryException, OutlookException { super(node.getPath(), node); } /** * {@inheritDoc} */ @Override public Folder addSubfolder(String name) throws RepositoryException, OutlookException { final Node parent = getNode(); Node subfolderNode = addFolder(parent, name, true); Folder subfolder = newFolder(this, subfolderNode); parent.save(); Set<Folder> subfolders = this.subfolders.get(); if (subfolders != null) { subfolders.add(subfolder); } return subfolder; } /** * {@inheritDoc} */ @Override protected Locale userLocale() { return currentUserLocale(); } /** * {@inheritDoc} */ @Override protected Folder newFolder(Folder parent, Node node) throws RepositoryException, OutlookException { Folder folder = new UserFolder(parent, node); return folder; } /** * {@inheritDoc} */ @Override protected Folder newFolder(String rootPath, Node node) throws RepositoryException, OutlookException { Folder folder = new UserFolder(rootPath, node); return folder; } /** * {@inheritDoc} */ @Override protected File newFile(Folder parent, Node node) throws RepositoryException, OutlookException { File file = new UserFile(parent, node); return file; } } /** * The Class PersonalDocumentsFolder. */ protected class PersonalDocumentsFolder extends UserFolder implements UserDocuments { /** * Instantiates a new personal documents folder. * * @param node the node * @throws RepositoryException the repository exception * @throws OutlookException the outlook exception */ protected PersonalDocumentsFolder(Node node) throws RepositoryException, OutlookException { super(node); } /** * {@inheritDoc} */ @Override public Folder getRootFolder() throws OutlookException { return this; } /** * {@inheritDoc} * */ @Override public Folder getFolder(String path) throws OutlookException, RepositoryException { String rootPath = getPath(); Folder folder; String folderPath = HierarchyNode.getPath(path); if (rootPath.equals(folderPath)) { folder = this; } else if (folderPath.startsWith(rootPath)) { Node node = node(folderPath); folder = new UserFolder(node.getParent().getPath(), node); } else { throw new BadParameterException("Path does not belong to space documents: " + path); } initDocumentLink(this, folder); return folder; } /** * {@inheritDoc} */ @Override public Collection<File> findAllLastDocuments(String text) throws RepositoryException, OutlookException { QueryManager qm = getNode().getSession().getWorkspace().getQueryManager(); Set<File> res = new LinkedHashSet<File>(); if (text == null || text.length() == 0) { Query q = qm.createQuery("SELECT * FROM nt:file WHERE exo:lastModifier='" + currentUserId() + "' ORDER BY exo:lastModifiedDate DESC, exo:title ASC", Query.SQL); final String currentUser = currentUserId(); // fetch and filter nodes: not trashed and not system fetchQuery(q.execute(), 20, res, node -> { try { // XXX skip some known system locations/names String path = node.getPath(); if (path.indexOf("/ApplicationData") >= 0 || path.indexOf("exo:applications") >= 0) { return false; } // skip trashed if (org.exoplatform.ecm.webui.utils.Utils.isInTrash(node)) { return false; } // access all nodes except of owned by root, but only if it is not his session AccessControlList acl = ((ExtendedNode) node).getACL(); String owner = acl.getOwner(); if (currentUser.equals(owner)) { return true; } else { return !ROOT_USER.equals(owner); } } catch (RepositoryException e) { // ignore it if (LOG.isDebugEnabled()) { LOG.debug("Error getting ACL/owner of " + node, e); } return false; } }); } else { Query qOwn = qm.createQuery("SELECT * FROM nt:file WHERE exo:lastModifier='" + currentUserId() + "' AND jcr:path LIKE '" + getPath() + "/%' AND exo:title LIKE '%" + text + "%' ORDER BY exo:lastModifiedDate DESC, exo:title ASC", Query.SQL); // fetch first three modified by this user only fetchQuery(qOwn.execute(), 3, res); // and add all others up to total 20 files Query qOthers = qm.createQuery( "SELECT * FROM nt:file WHERE jcr:path LIKE '" + getPath() + "/%' AND exo:title LIKE '%" + text + "%' ORDER BY exo:lastModifiedDate DESC, exo:title ASC", Query.SQL); fetchQuery(qOthers.execute(), 20 - res.size(), res); } if (res.size() < 20) { // If have some space, then fetch latest from spaces StringBuilder sql = new StringBuilder(); sql.append("SELECT * FROM nt:file WHERE jcr:path LIKE '"); sql.append(SPACES_HOME); sql.append("/%/Documents/%'"); if (text != null && text.length() > 0) { sql.append(" AND exo:title LIKE '%"); sql.append(text); sql.append("%'"); } sql.append(" ORDER BY exo:lastModifiedDate DESC, exo:title ASC"); Query qSpaces = qm.createQuery(sql.toString(), Query.SQL); fetchQuery(qSpaces.execute(), 20 - res.size(), res); } return res; } /** * {@inheritDoc} */ @Override public Collection<File> findLastDocuments(String text) throws RepositoryException, OutlookException { QueryManager qm = getNode().getSession().getWorkspace().getQueryManager(); Set<File> res = new LinkedHashSet<File>(); // TODO this search will not include files from user's Public folder Query q; if (text == null || text.length() == 0) { q = qm.createQuery("SELECT * FROM nt:file WHERE jcr:path LIKE '" + getPath() + "/%' ORDER BY exo:lastModifiedDate DESC, exo:title ASC", Query.SQL); } else { q = qm.createQuery("SELECT * FROM nt:file WHERE jcr:path LIKE '" + getPath() + "/%' AND exo:title LIKE '%" + text + "%'", Query.SQL); } fetchQuery(q.execute(), 20, res); return res; } /** * Gets the drive name. * * @return the drive name */ protected String getDriveName() { // XXX we use what pointed in XML config return PERSONAL_DOCUMENTS; } } /** * The Class UserFile. */ protected class UserFile extends File { /** * Instantiates a new user file. * * @param parent the parent * @param node the node * @throws RepositoryException the repository exception * @throws OutlookException the outlook exception */ protected UserFile(Folder parent, Node node) throws RepositoryException, OutlookException { super(parent, node); } /** * Instantiates a new user file. * * @param parentPath the parent path * @param node the node * @throws RepositoryException the repository exception * @throws OutlookException the outlook exception */ protected UserFile(String parentPath, Node node) throws RepositoryException, OutlookException { super(parentPath, node); } /** * {@inheritDoc} */ @Override protected Locale userLocale() { return currentUserLocale(); } } /** * The Class UserImpl. */ protected class UserImpl extends OutlookUser { /** The social identity manager. */ protected final IdentityManager socialIdentityManager; /** The social activity manager. */ protected final ActivityManager socialActivityManager; /** * Instantiates a new user impl. * * @param email the email * @param displayName the display name * @param userName the user name */ protected UserImpl(String email, String displayName, String userName) { super(email, displayName, userName); this.socialIdentityManager = socialIdentityManager(); this.socialActivityManager = socialActivityManager(); } /** * {@inheritDoc} */ @Override public ExoSocialActivity postActivity(OutlookMessage message) throws OutlookException { // save text to user documents try { Node userDocs = userDocumentsNode(localUser); if (userDocs != null) { Node userPublicFolder = userDocs.getParent().getNode("Public"); Node messagesFolder = messagesFolder(userPublicFolder, localUser, "member:/platform/users"); Node messageFile = addMessageFile(messagesFolder, message); setPermissions(messageFile, localUser, "member:/platform/users"); messagesFolder.save(); message.setFileNode(messageFile); String userMessage = message.getTitle(); userMessage = userMessage != null && userMessage.length() > 0 ? safeText(userMessage) : null; final String origType = org.exoplatform.wcm.ext.component.activity.listener.Utils .getActivityType(); try { org.exoplatform.wcm.ext.component.activity.listener.Utils .setActivityType(OutlookMessageActivity.ACTIVITY_TYPE); ExoSocialActivity activity = org.exoplatform.wcm.ext.component.activity.listener.Utils .postFileActivity(messageFile, userMessage, true, false, "", ""); // TODO should we care about activity removal with the message file? activity.setPermanLink(LinkProvider.getSingleActivityUrl(activity.getId())); return activity; } finally { org.exoplatform.wcm.ext.component.activity.listener.Utils.setActivityType(origType); } } else { throw new OutlookException("Has no Personal Documents folder for user " + localUser); } } catch (Exception e) { throw new OutlookException("Error posting activity for user " + localUser, e); } } /** * {@inheritDoc} */ @Override public ExoSocialActivity postActivity(String title, String body) throws Exception { // post activity to user status stream Identity userIdentity = socialIdentityManager.getOrCreateIdentity(OrganizationIdentityProvider.NAME, currentUserId(), true); String safeTitle = safeText(title); String safeBody = safeHtml(body); ExoSocialActivity activity = new ExoSocialActivityImpl(userIdentity.getId(), null, safeTitle, safeBody); socialActivityManager.saveActivityNoReturn(userIdentity, activity); activity.setPermanLink(LinkProvider.getSingleActivityUrl(activity.getId())); return activity; } /** * {@inheritDoc} */ @Override public ExoSocialActivity postActivity(String text) throws Exception { // post activity to user status stream Identity userIdentity = socialIdentityManager.getOrCreateIdentity(OrganizationIdentityProvider.NAME, currentUserId(), true); String safeText = safeActivityMessage(text); ExoSocialActivity activity = new ExoSocialActivityImpl(userIdentity.getId(), PeopleService.PEOPLE_APP_ID, safeText, null); // we do like done UIDefaultActivityComposer activity.setType(UIDefaultActivity.ACTIVITY_TYPE); socialActivityManager.saveActivityNoReturn(userIdentity, activity); activity.setPermanLink(LinkProvider.getSingleActivityUrl(activity.getId())); return activity; } /** * {@inheritDoc} */ @Override public Page addWikiPage(OutlookMessage message) throws Exception { String wikiType = PortalConfig.PORTAL_TYPE; String creator = message.getUser().getLocalUser(); List<String> users = new ArrayList<String>(); users.add(creator); // TODO add space group to users? return createWikiPage(wikiType, "intranet", creator, message.getSubject(), messageSummary(message), message.getBody(), users); } /** * {@inheritDoc} */ @Override public Topic addForumTopic(String categoryId, String forumId, OutlookMessage message) throws Exception { return createForumTopic(categoryId, forumId, message.getUser().getLocalUser(), message.getSubject(), messageSummary(message), message.getBody()); } } /** * The Class OutlookSpaceImpl. */ protected class OutlookSpaceImpl extends OutlookSpace { /** * The Class SpaceFolder. */ class SpaceFolder extends UserFolder { /** * Instantiates a new space folder. * * @param parent the parent * @param node the node * @throws RepositoryException the repository exception * @throws OutlookException the outlook exception */ protected SpaceFolder(Folder parent, Node node) throws RepositoryException, OutlookException { super(parent, node); } /** * Instantiates a new space folder. * * @param rootPath the root path * @param node the node * @throws RepositoryException the repository exception * @throws OutlookException the outlook exception */ protected SpaceFolder(String rootPath, Node node) throws RepositoryException, OutlookException { super(rootPath, node); } /** * {@inheritDoc} */ @Override protected void readChildNodes() throws RepositoryException, OutlookException { super.readChildNodes(); for (Folder sf : this.subfolders.get()) { initDocumentLink(OutlookSpaceImpl.this, sf); } for (File f : this.files.get()) { initDocumentLink(OutlookSpaceImpl.this, f); } } } /** * The Class RootFolder. */ class RootFolder extends SpaceFolder { /** * Instantiates a new root folder. * * @param rootPath the root path * @param node the node * @throws RepositoryException the repository exception * @throws OutlookException the outlook exception */ protected RootFolder(String rootPath, Node node) throws RepositoryException, OutlookException { super(rootPath, node); initDocumentLink(OutlookSpaceImpl.this, this); hasSubfolders(); // force child reading to init default folder in readSubnodes() } /** * {@inheritDoc} */ @Override protected Set<Folder> readSubnodes() throws RepositoryException, OutlookException { Folder uploads = null; Set<Folder> subfolders = super.readSubnodes(); for (Folder sf : subfolders) { if (sf.getTitle().equals(UPLAODS_FOLDER_TITLE)) { uploads = defaultSubfolder = sf; break; } } if (uploads == null) { try { Node subfolderNode = addFolder(node, UPLAODS_FOLDER_TITLE, false); uploads = newFolder(this, subfolderNode); node.save(); initDocumentLink(OutlookSpaceImpl.this, uploads); subfolders.add(uploads); defaultSubfolder = uploads; } catch (AccessDeniedException e) { if (LOG.isDebugEnabled()) { // gather some info about the user for the log String currentUserId = currentUserId(); StringBuilder userInfo = new StringBuilder(); userInfo.append(currentUserId); try { userInfo.append('['); for (Membership m : organization.getMembershipHandler() .findMembershipsByUser(currentUserId())) { userInfo.append(m.getMembershipType()); userInfo.append(':'); userInfo.append(m.getGroupId()); userInfo.append(' '); } userInfo.setCharAt(userInfo.length() - 1, ']'); } catch (Exception oe) { LOG.warn("Error getting organization user " + currentUserId, e); } LOG.debug("Error creating " + UPLAODS_FOLDER_TITLE + " folder in " + getPath() + ". User: " + userInfo.toString() + ". Parent node: " + node, e); // TODO we don't want throw Access error here, it should be thrown where actually will affect an // user // throw new AccessException("Access denied to " + OutlookSpaceImpl.this.getTitle(), e); } defaultSubfolder = null; } } return subfolders; } } /** The root path. */ protected final String rootPath; /** The root folder. */ protected final ThreadLocal<RootFolder> rootFolder = new ThreadLocal<RootFolder>(); /** The social identity manager. */ protected final IdentityManager socialIdentityManager; /** The social activity manager. */ protected final ActivityManager socialActivityManager; /** * Instantiates a new outlook space impl. * * @param socialSpace the social space * @throws RepositoryException the repository exception * @throws OutlookException the outlook exception */ protected OutlookSpaceImpl(Space socialSpace) throws RepositoryException, OutlookException { super(socialSpace.getGroupId(), socialSpace.getDisplayName(), socialSpace.getShortName(), socialSpace.getPrettyName()); this.rootPath = groupDocsPath(groupId); this.socialIdentityManager = socialIdentityManager(); this.socialActivityManager = socialActivityManager(); } /** * {@inheritDoc} */ @Override public Folder getFolder(String path) throws OutlookException, RepositoryException { Folder parent = getRootFolder(); Folder folder; String folderPath = HierarchyNode.getPath(path); if (rootPath.equals(folderPath)) { folder = parent; } else if (folderPath.startsWith(rootPath)) { folder = new SpaceFolder(parent, node(folderPath)); } else { throw new BadParameterException("Path does not belong to space documents: " + path); } initDocumentLink(this, folder); return folder; } /** * {@inheritDoc} */ @Override public Folder getRootFolder() throws OutlookException, RepositoryException { RootFolder root = rootFolder.get(); if (root != null) { // ensure folder's node valid try { root.getNode().getIndex(); return root; } catch (InvalidItemStateException e) { // it's invalid } } root = new RootFolder(rootPath, node(rootPath)); rootFolder.set(root); return root; } /** * {@inheritDoc} */ @Override public Collection<File> findLastDocuments(String text) throws RepositoryException, OutlookException { Folder root = getRootFolder(); QueryManager qm = root.getNode().getSession().getWorkspace().getQueryManager(); Set<File> res = new LinkedHashSet<File>(); if (text == null || text.length() == 0) { Query qOwn = qm.createQuery( "SELECT * FROM nt:file WHERE exo:lastModifier='" + currentUserId() + "' AND jcr:path LIKE '" + root.getPath() + "/%' ORDER BY exo:lastModifiedDate DESC, exo:title ASC", Query.SQL); // fetch first three modified by this user only fetchQuery(qOwn.execute(), 3, res); // and add all others up to total 20 files Query qOthers = qm.createQuery("SELECT * FROM nt:file WHERE jcr:path LIKE '" + root.getPath() + "/%' ORDER BY exo:lastModifiedDate DESC, exo:title ASC", Query.SQL); fetchQuery(qOthers.execute(), 20 - res.size(), res); } else { Query qOwn = qm.createQuery("SELECT * FROM nt:file WHERE exo:lastModifier='" + currentUserId() + "' AND jcr:path LIKE '" + root.getPath() + "/%' AND exo:title LIKE '%" + text + "%' ORDER BY exo:lastModifiedDate DESC, exo:title ASC", Query.SQL); // fetch first three modified by this user only fetchQuery(qOwn.execute(), 3, res); // and add all others up to total 20 files Query qOthers = qm.createQuery( "SELECT * FROM nt:file WHERE jcr:path LIKE '" + root.getPath() + "/%' AND exo:title LIKE '%" + text + "%' ORDER BY exo:lastModifiedDate DESC, exo:title ASC", Query.SQL); fetchQuery(qOthers.execute(), 20 - res.size(), res); } return res; } /** * {@inheritDoc} */ @Override public ExoSocialActivity postActivity(OutlookMessage message) throws Exception { Node spaceDocs = spaceDocumentsNode(groupId); Node messagesFolder = messagesFolder(spaceDocs, groupId); Node messageFile = addMessageFile(messagesFolder, message); setPermissions(messageFile, new StringBuilder("member:").append(groupId).toString()); messagesFolder.save(); message.setFileNode(messageFile); String userMessage = message.getTitle(); userMessage = userMessage != null && userMessage.length() > 0 ? safeText(userMessage) : null; final String origType = org.exoplatform.wcm.ext.component.activity.listener.Utils.getActivityType(); try { org.exoplatform.wcm.ext.component.activity.listener.Utils .setActivityType(OutlookMessageActivity.ACTIVITY_TYPE); ExoSocialActivity activity = org.exoplatform.wcm.ext.component.activity.listener.Utils .postFileActivity(messageFile, userMessage, true, false, "", ""); // TODO should we care about activity removal with the message file? activity.setPermanLink(LinkProvider.getSingleActivityUrl(activity.getId())); return activity; } finally { org.exoplatform.wcm.ext.component.activity.listener.Utils.setActivityType(origType); } } /** * {@inheritDoc} */ @Override public ExoSocialActivity postActivity(OutlookUser user, String title, String body) throws Exception { // post activity to space status stream under current user Identity spaceIdentity = socialIdentityManager.getOrCreateIdentity(SpaceIdentityProvider.NAME, this.prettyName, true); Identity userIdentity = socialIdentityManager.getOrCreateIdentity(OrganizationIdentityProvider.NAME, currentUserId(), true); String safeTitle = safeText(title); String safeBody = safeHtml(body); ExoSocialActivity activity = new ExoSocialActivityImpl(userIdentity.getId(), SpaceActivityPublisher.SPACE_APP_ID, safeTitle, safeBody); socialActivityManager.saveActivityNoReturn(spaceIdentity, activity); activity.setPermanLink(LinkProvider.getSingleActivityUrl(activity.getId())); return activity; } /** * {@inheritDoc} */ @Override public ExoSocialActivity postActivity(OutlookUser user, String text) throws Exception { // post activity to space status stream under current user Identity spaceIdentity = socialIdentityManager.getOrCreateIdentity(SpaceIdentityProvider.NAME, this.prettyName, true); Identity userIdentity = socialIdentityManager.getOrCreateIdentity(OrganizationIdentityProvider.NAME, currentUserId(), true); String safeText = safeActivityMessage(text); ExoSocialActivity activity = new ExoSocialActivityImpl(userIdentity.getId(), SpaceActivityPublisher.SPACE_APP_ID, safeText, null); // we do like done UIDefaultActivityComposer activity.setType(UIDefaultActivity.ACTIVITY_TYPE); socialActivityManager.saveActivityNoReturn(spaceIdentity, activity); activity.setPermanLink(LinkProvider.getSingleActivityUrl(activity.getId())); return activity; } /** * {@inheritDoc} */ @Override public Page addWikiPage(OutlookMessage message) throws Exception { String wikiType = PortalConfig.GROUP_TYPE; String creator = message.getUser().getLocalUser(); List<String> users = new ArrayList<String>(); users.add(creator); // TODO add space group to users? return createWikiPage(wikiType, getGroupId(), creator, message.getSubject(), messageSummary(message), message.getBody(), users); } /** * {@inheritDoc} */ @Override public Topic addForumTopic(OutlookMessage message) throws Exception { String creator = message.getUser().getLocalUser(); // Group group = organization.getGroupHandler().findGroupById(getGroupId()); String parentGrId = group.getParentId(); // Category must exists as we are running against existing space String categoryId = org.exoplatform.forum.service.Utils.CATEGORY + parentGrId.replaceAll(CommonUtils.SLASH, CommonUtils.EMPTY_STR); // Forum must exists as we are running against existing space String forumId = org.exoplatform.forum.service.Utils.FORUM_SPACE_ID_PREFIX + group.getGroupName(); // return createForumTopic(categoryId, forumId, creator, message.getSubject(), messageSummary(message), message.getBody()); } /** * {@inheritDoc} */ @Override public Topic addForumTopic(OutlookUser user, String name, String text) throws Exception { String creator = user.getLocalUser(); // Group group = organization.getGroupHandler().findGroupById(getGroupId()); String parentGrId = group.getParentId(); // Category must exists as we are running against existing space String categoryId = org.exoplatform.forum.service.Utils.CATEGORY + parentGrId.replaceAll(CommonUtils.SLASH, CommonUtils.EMPTY_STR); // Forum must exists as we are running against existing space String forumId = org.exoplatform.forum.service.Utils.FORUM_SPACE_ID_PREFIX + group.getGroupName(); // return createForumTopic(categoryId, forumId, creator, name, null, text); } } /** The jcr service. */ protected final RepositoryService jcrService; /** The session providers. */ protected final SessionProviderService sessionProviders; /** The finder. */ protected final NodeFinder finder; /** The hierarchy creator. */ protected final NodeHierarchyCreator hierarchyCreator; /** The organization. */ protected final OrganizationService organization; /** The drive service. */ protected final ManageDriveService driveService; /** The listener service. */ protected final ListenerService listenerService; /** The wiki service. */ protected final WikiService wikiService; /** The forum service. */ protected final ForumService forumService; /** The trash service. */ protected final TrashService trashService; /** The wiki rendering service. */ protected final RenderingService wikiRenderingService; /** The resource bundle service. */ protected final ResourceBundleService resourceBundleService; /** The html policy. */ protected final PolicyFactory htmlPolicy = Sanitizers.BLOCKS.and(Sanitizers.FORMATTING).and(Sanitizers.IMAGES) .and(Sanitizers.LINKS).and(Sanitizers.TABLES) // with // extra // attributes // for // tables // (MS // loves // to use // them // for HTML // re-formating) .and(new HtmlPolicyBuilder().allowStandardUrlProtocols().allowElements("table", "th", "tr", "td") .allowAttributes("border", "cellpadding", "cellspacing", "width", "height").onElements("table") .allowAttributes("bgcolor", "width", "height", "colspan", "rowspan") .onElements("td", "tr", "th").toFactory()) .and(Sanitizers.STYLES); /** The text policy. */ protected final PolicyFactory textPolicy = new HtmlPolicyBuilder().toFactory(); /** * Custom policy to allow supported elements in activity text as described in * <a href= * "https://www.exoplatform.com/docs/PLF43/PLFUserGuide.GettingStarted.ActivitiesInActivityStream.HTMLTags.html"> * Platform User Guide</a> */ protected final PolicyFactory activityPolicy = new HtmlPolicyBuilder().allowUrlProtocols("http", "https") .allowElements("b", "i", "a", "span", "em", "strong", "p", "ol", "ul", "li", "br", "img", "blockquote", "q") .allowAttributes("href").onElements("a").allowAttributes("target").matching(true, "_blank") .onElements("a").allowAttributes("alt", "src").onElements("img").toFactory(); /** The link with href not a hash in local document target. */ protected final Pattern linkNotLocal = Pattern.compile("href=['\"][^#][.\\w\\W\\S]*?['\"]", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL); /** The link with target. */ protected final Pattern linkWithTarget = Pattern.compile("<a(?=\\s).*?(target=['\"].*?['\"])[^>]*>", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL); /** The link without target. */ protected final Pattern linkWithoutTarget = Pattern.compile( "<a(?=\\s)(?:(?!target=).)*?([.\\W\\w\\S\\s[^>]])*?(>)", Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL); /** * Authenticated users. */ protected final ConcurrentHashMap<String, OutlookUser> authenticated = new ConcurrentHashMap<String, OutlookUser>(); /** * Spaces cache. * TODO There is an issue with threads when different requests reuse them. Space's root node may be already * invalid. See also in getRootFolder(). */ protected final ConcurrentHashMap<String, OutlookSpaceImpl> spaces = new ConcurrentHashMap<String, OutlookSpaceImpl>(); /** The mailserver api. */ protected MailAPI mailserverApi; /** The trash home path. */ protected String trashHomePath; /** * Outlook service with storage in JCR and with managed features. * * @param jcrService {@link RepositoryService} * @param sessionProviders {@link SessionProviderService} * @param hierarchyCreator {@link NodeHierarchyCreator} * @param finder {@link NodeFinder} * @param organization {@link OrganizationService} * @param listenerService {@link ListenerService} * @param driveService {@link ManageDriveService} * @param trashService {@link TrashService} * @param wikiService {@link WikiService} * @param forumService {@link ForumService} * @param wikiRenderingService {@link RenderingService} * @param resourceBundleService {@link ResourceBundleService} * @param params {@link InitParams} * @throws ConfigurationException when parameters configuration error * @throws MailServerException when Mail server API error */ public OutlookServiceImpl(RepositoryService jcrService, SessionProviderService sessionProviders, NodeHierarchyCreator hierarchyCreator, NodeFinder finder, OrganizationService organization, ListenerService listenerService, ManageDriveService driveService, TrashService trashService, WikiService wikiService, ForumService forumService, RenderingService wikiRenderingService, ResourceBundleService resourceBundleService, InitParams params) throws ConfigurationException, MailServerException { this.jcrService = jcrService; this.sessionProviders = sessionProviders; this.hierarchyCreator = hierarchyCreator; this.finder = finder; this.organization = organization; this.driveService = driveService; this.listenerService = listenerService; this.wikiService = wikiService; this.forumService = forumService; this.trashService = trashService; this.wikiRenderingService = wikiRenderingService; this.resourceBundleService = resourceBundleService; // API for user requests (uses credentials from eXo user profile) MailAPI api = new MailAPI(); this.mailserverApi = api; } /** * {@inheritDoc} */ @Override @Deprecated public Folder getFolder(String path) throws OutlookException, RepositoryException { Node node = node(path); Folder folder = new UserFolder(path, node); return folder; } /** * {@inheritDoc} */ @Override @Deprecated public Folder getFolder(Folder parent, String path) throws OutlookException, RepositoryException { Node node = node(path); return new UserFolder(parent, node); } /** * {@inheritDoc} */ @Override public List<File> saveAttachment(OutlookSpace space, Folder destFolder, OutlookUser user, String comment, String messageId, String attachmentToken, String... attachmentIds) throws OutlookException, RepositoryException { List<File> files = new ArrayList<File>(); Node parent = destFolder.getNode(); for (String attachmentId : attachmentIds) { JsonValue vatt = mailserverApi.getAttachment(user, messageId, attachmentToken, attachmentId); JsonValue vName = vatt.getElement("Name"); if (isNull(vName)) { throw new OutlookFormatException("Attachment doesn't contain Name"); } String name = vName.getStringValue(); JsonValue vContentType = vatt.getElement("ContentType"); if (isNull(vContentType)) { throw new OutlookFormatException("Attachment (" + name + ") doesn't contain ContentType"); } String contentType = vContentType.getStringValue(); JsonValue vContentBytes = vatt.getElement("ContentBytes"); if (isNull(vContentBytes)) { throw new OutlookFormatException("Attachment (" + name + ") doesn't contain ContentBytes"); } // FYI attachment content in BASE64 (may be twice!) String contentBytes = vContentBytes.getStringValue(); byte[] decoded = Base64.decodeBase64(contentBytes); // Save in JCR: content goes as-is here try (InputStream contentStream = new ByteArrayInputStream(decoded)) { Node attachmentNode = addFile(parent, name, contentType, contentStream); if (space != null) { setPermissions(attachmentNode, new StringBuilder("member:").append(space.getGroupId()).toString()); } else { setPermissions(attachmentNode, user.getLocalUser(), "member:/platform/users"); } files.add(new UserFile(destFolder, attachmentNode)); } catch (IOException e) { throw new OutlookException("Error saving attachment in a file " + name, e); } } parent.save(); // save everything at the end only // TODO care about activity removal with the message file postAttachmentActivity(destFolder, files, user, safeText(comment)); if (space != null) { for (File f : files) { initDocumentLink(space, f); } } return files; } /** * {@inheritDoc} */ @Override public List<File> saveAttachment(Folder destFolder, OutlookUser user, String comment, String messageId, String attachmentToken, String... attachmentIds) throws OutlookException, RepositoryException { return saveAttachment(null, destFolder, user, comment, messageId, attachmentToken, attachmentIds); } /** * {@inheritDoc} */ @Override public OutlookEmail getAddress(String email, String displayName) throws OutlookException { return new OutlookEmail(email, displayName); } /** * {@inheritDoc} */ @Override public OutlookUser getUser(String email, String displayName, String ewsUrl) throws OutlookException, RepositoryException { ConversationState contextState = ConversationState.getCurrent(); if (contextState != null) { String exoUsername = contextState.getIdentity().getUserId(); if (!IdentityConstants.ANONIM.equals(exoUsername)) { URI mailServerUrl; if (ewsUrl != null) { try { URI ewsUri = new URI(ewsUrl); String host = ewsUri.getHost(); String scheme = ewsUri.getScheme(); int port = ewsUri.getPort(); // TODO do we need remove obvious things? // if (port >= 0) { // if (port == 443 && "https".equalsIgnoreCase(scheme)) { // port = -1; // } else if (port == 80 && "http".equalsIgnoreCase(scheme)) { // port = -1; // } // } mailServerUrl = new URI(scheme, null, host, port, null, null, null); } catch (URISyntaxException e) { throw new MailServerException("Error parsing EWS API URL " + ewsUrl, e); } } else { mailServerUrl = null; } OutlookUser user = authenticated.get(exoUsername); if (user == null) { // new user instance user = new UserImpl(email, displayName, exoUsername); // save user in map of authenticated for later use (multi-thread) authenticated.put(exoUsername, user); } if (email != null) { user.setEmail(email); } if (mailServerUrl != null) { user.setMailServerUrl(mailServerUrl); } return user; } } return null; } /** * {@inheritDoc} */ @Override public OutlookMessage buildMessage(String id, OutlookUser user, OutlookEmail from, List<OutlookEmail> to, Calendar created, Calendar modified, String title, String subject, String body) throws OutlookException { OutlookMessage message = new OutlookMessage(user); message.setId(id); message.setFrom(from); message.setTo(to); message.setTitle(title); message.setSubject(subject); message.setBody(body); message.setCreated(created); message.setModified(modified); return message; } /** * {@inheritDoc} */ @Override public OutlookMessage getMessage(OutlookUser user, String messageId, String messageToken) throws OutlookException { // Read message from Exchange server by ID JsonValue vatt = mailserverApi.getMessage(user, messageId, messageToken); JsonValue vSubject = vatt.getElement("Subject"); if (isNull(vSubject)) { throw new OutlookFormatException("Message " + messageId + " doesn't contain Subject"); } String subject = vSubject.getStringValue(); JsonValue vTo = vatt.getElement("ToRecipients"); if (isNull(vTo)) { throw new OutlookFormatException( "Message (" + messageId + " : " + subject + ") doesn't contain ToRecipients"); } if (!vTo.isArray()) { throw new OutlookFormatException( "Message (" + messageId + " : " + subject + ")'s ToRecipients isn't an array"); } List<OutlookEmail> to = new ArrayList<OutlookEmail>(vTo.size()); for (Iterator<JsonValue> toiter = vTo.getElements(); toiter.hasNext();) { to.add(readEmail(toiter.next())); } OutlookEmail from; JsonValue vFrom = vatt.getElement("From"); if (isNull(vFrom)) { from = null; } else { from = readEmail(vFrom); } JsonValue vCreatedDateTime = vatt.getElement("CreatedDateTime"); if (isNull(vCreatedDateTime)) { throw new OutlookFormatException( "Message (" + messageId + " : " + subject + ") doesn't contain CreatedDateTime"); } Calendar created = Calendar.getInstance(); try { created.setTime(OutlookMessage.DATE_FORMAT.parse(vCreatedDateTime.getStringValue())); } catch (ParseException e) { LOG.error("Error parsing message date " + vCreatedDateTime.getStringValue(), e); } JsonValue vLastModifiedDateTime = vatt.getElement("LastModifiedDateTime"); if (isNull(vLastModifiedDateTime)) { throw new OutlookFormatException( "Message (" + messageId + " : " + subject + ") doesn't contain LastModifiedDateTime"); } Calendar modified = Calendar.getInstance(); try { modified.setTime(OutlookMessage.DATE_FORMAT.parse(vLastModifiedDateTime.getStringValue())); } catch (ParseException e) { LOG.error("Error parsing message date " + vCreatedDateTime.getStringValue(), e); } JsonValue vBody = vatt.getElement("Body"); if (isNull(vBody)) { throw new OutlookFormatException("Message (" + messageId + " : " + subject + ") doesn't contain Body"); } JsonValue vContentType = vBody.getElement("ContentType"); if (isNull(vContentType)) { throw new OutlookFormatException( "Message (" + messageId + " : " + subject + ")'s body doesn't contain ContentType"); } String contentType = vContentType.getStringValue(); JsonValue vContent = vBody.getElement("Content"); if (isNull(vContent)) { throw new OutlookFormatException( "Message (" + messageId + " : " + subject + ")'s body doesn't contain Content"); } String content = vContent.getStringValue(); OutlookMessage message = new OutlookMessage(user); message.setId(messageId); message.setFrom(from); message.setTo(to); message.setSubject(subject); message.setBody(content); message.setType(contentType); message.setCreated(created); message.setModified(modified); return message; } /** * On-start initializer. */ @Override public void start() { try { this.trashHomePath = trashService.getTrashHomeNode().getPath(); } catch (RepositoryException e) { LOG.warn("Error getting Trash home node", e); this.trashHomePath = "/Trash"; } LOG.info("Outlook service successfuly started"); } /** * On-stop finalizer. */ @Override public void stop() { authenticated.clear(); try { mailserverApi.close(); LOG.info("Outlook service successfuly stopped"); } catch (MailServerException e) { LOG.warn("Outlook service stop encountered with API error", e); } } /** * {@inheritDoc} */ @Override public OutlookSpace getSpace(String groupId) throws OutlookSpaceException, RepositoryException, OutlookException { OutlookSpaceImpl space = spaces.get(groupId); if (space == null) { Space socialSpace = spaceService().getSpaceByGroupId(groupId); if (socialSpace != null) { space = new OutlookSpaceImpl(socialSpace); spaces.put(socialSpace.getGroupId(), space); } } return space; } /** * {@inheritDoc} */ @Override public List<OutlookSpace> getUserSpaces() throws OutlookSpaceException { return userSpaces(currentUserId()); } /** * {@inheritDoc} */ @Override public UserDocuments getUserDocuments() throws RepositoryException, OutlookException { // get Personal Documents folder String userName = currentUserId(); try { Node userDocsNode = userDocumentsNode(userName); PersonalDocumentsFolder folder = new PersonalDocumentsFolder(userDocsNode); initDocumentLink(folder, folder); return folder; } catch (Exception e) { throw new OutlookException("Error reading user's Personal Documents node for " + userName, e); } } // *********************** testing level ********************** /** * Sets the api. * * @param mockedAPI the new api */ void setAPI(MailAPI mockedAPI) { this.mailserverApi = mockedAPI; } // *********************** implementation level *************** /** * Node title. * * @param node the node * @return the string * @throws RepositoryException the repository exception */ protected String nodeTitle(Node node) throws RepositoryException { return node.getProperty(NodetypeConstant.EXO_TITLE).getString(); } /** * Node content. * * @param node the node * @return the node * @throws RepositoryException the repository exception */ protected Node nodeContent(Node node) throws RepositoryException { return node.getNode("jcr:content"); } /** * Node created. * * @param node the node * @return the calendar * @throws RepositoryException the repository exception */ protected Calendar nodeCreated(Node node) throws RepositoryException { return node.getProperty("jcr:created").getDate(); } /** * Mime type. * * @param content the content * @return the string * @throws RepositoryException the repository exception */ protected String mimeType(Node content) throws RepositoryException { return content.getProperty("jcr:mimeType").getString(); } /** * Data. * * @param content the content * @return the property * @throws RepositoryException the repository exception */ protected Property data(Node content) throws RepositoryException { return content.getProperty("jcr:data"); } /** * Generate id. * * @param workspace the workspace * @param path the path * @return the uuid */ protected UUID generateId(String workspace, String path) { StringBuilder s = new StringBuilder(); s.append(workspace); s.append(path); s.append(System.currentTimeMillis()); s.append(String.valueOf(RANDOM.nextLong())); return UUID.nameUUIDFromBytes(s.toString().getBytes()); } /** * Gets the exo user. * * @param userName the user name * @return the exo user * @throws OutlookException the outlook exception */ protected org.exoplatform.services.organization.User getExoUser(String userName) throws OutlookException { try { return organization.getUserHandler().findUserByName(userName); } catch (Exception e) { throw new OutlookException("Error searching user " + userName, e); } } /** * Node. * * @param nodePath the node path * @return the node * @throws BadParameterException the bad parameter exception * @throws RepositoryException the repository exception */ protected Node node(String nodePath) throws BadParameterException, RepositoryException { String workspace, path; if (nodePath.startsWith("/")) { workspace = jcrService.getCurrentRepository().getConfiguration().getDefaultWorkspaceName(); path = nodePath; } else { // TODO it's a not used experimental thing, see also ContentLink component int i = nodePath.indexOf('/'); if (i > 0) { workspace = nodePath.substring(0, i); path = nodePath.substring(i); } else { throw new BadParameterException("Invalid path " + nodePath); } } return node(workspace, path); } /** * Node. * * @param workspace the workspace * @param path the path * @return the node * @throws BadParameterException the bad parameter exception * @throws RepositoryException the repository exception */ protected Node node(String workspace, String path) throws BadParameterException, RepositoryException { SessionProvider sp = sessionProviders.getSessionProvider(null); Session userSession = sp.getSession(workspace, jcrService.getCurrentRepository()); Item item = finder.findItem(userSession, path); if (item.isNode()) { return (Node) item; } else { throw new BadParameterException("Not a node " + path); } } /** * System node. * * @param workspace the workspace * @param path the path * @return the node * @throws BadParameterException the bad parameter exception * @throws RepositoryException the repository exception */ protected Node systemNode(String workspace, String path) throws BadParameterException, RepositoryException { SessionProvider sp = sessionProviders.getSystemSessionProvider(null); Session sysSession = sp.getSession(workspace, jcrService.getCurrentRepository()); Item item = finder.findItem(sysSession, path); if (item.isNode()) { return (Node) item; } else { throw new BadParameterException("Not a node " + path); } } /** * Checkout. * * @param node the node * @return true, if successful * @throws RepositoryException the repository exception */ protected boolean checkout(Node node) throws RepositoryException { if (node.isNodeType("mix:versionable")) { if (!node.isCheckedOut()) { node.checkout(); } return true; } else { return false; } } /** * Checks if is null. * * @param json the json * @return true, if is null */ protected boolean isNull(JsonValue json) { return json == null || json.isNull(); } /** * Checks if is not null. * * @param json the json * @return true, if is not null */ protected boolean isNotNull(JsonValue json) { return json != null && !json.isNull(); } /** * Current user locale. * * @return the locale */ protected Locale currentUserLocale() { WebuiRequestContext context = WebuiRequestContext.getCurrentInstance(); return context != null ? context.getLocale() : null; } /** * Add nt:file node for given content stream and title. If a node with such name exists a new name will be * generated by adding a numerical index to the end. * * @param parent {@link Node} * @param title {@link String} * @param contentType {@link String} * @param content {@link InputStream} * @return {@link Node} * @throws RepositoryException when storage error */ protected Node addFile(Node parent, String title, String contentType, InputStream content) throws RepositoryException { Node file; String baseName = cleanName(title); String name = baseName; int siblingNumber = 0; do { try { file = parent.getNode(name); // such node already exists - find new name for the file (by adding sibling index to the end) siblingNumber++; int extIndex = baseName.lastIndexOf("."); if (extIndex > 0 && extIndex != baseName.length() - 1) { String jcrName = baseName.substring(0, extIndex); String jcrExt = baseName.substring(extIndex + 1); name = new StringBuilder(jcrName).append('-').append(siblingNumber).append('.').append(jcrExt) .toString(); } else { name = new StringBuilder(baseName).append('-').append(siblingNumber).toString(); } } catch (PathNotFoundException e) { // no such node exists, add it using internalName created by CD's cleanName() file = parent.addNode(name, "nt:file"); break; } } while (true); Node resource = file.addNode("jcr:content", "nt:resource"); resource.setProperty("jcr:mimeType", contentType != null ? contentType : ContentType.APPLICATION_OCTET_STREAM.getMimeType()); Calendar fileDate = Calendar.getInstance(); resource.setProperty("jcr:lastModified", fileDate); resource.setProperty("jcr:data", content); if (siblingNumber > 0) { int extIndex = title.lastIndexOf("."); if (extIndex > 0 && extIndex != title.length() - 1) { String titleName = title.substring(0, extIndex); String titleExt = title.substring(extIndex + 1); title = new StringBuilder(titleName).append(" (").append(siblingNumber).append(").") .append(titleExt).toString(); } else { title = new StringBuilder(title).append(" (").append(siblingNumber).append(')').toString(); } } if (!file.hasProperty(NodetypeConstant.EXO_TITLE)) { file.addMixin(NodetypeConstant.EXO_RSS_ENABLE); } file.setProperty(NodetypeConstant.EXO_TITLE, title); // file.setProperty("exo:summary", summary); try { file.setProperty(NodetypeConstant.EXO_NAME, title); } catch (ConstraintViolationException | ValueFormatException e) { LOG.warn("Cannot set exo:name property to '" + title + "' for file " + file.getPath() + ": " + e); } if (file.isNodeType(NodetypeConstant.EXO_DATETIME)) { file.setProperty(NodetypeConstant.EXO_DATE_CREATED, fileDate); file.setProperty(NodetypeConstant.EXO_DATE_MODIFIED, fileDate); } if (file.isNodeType(NodetypeConstant.EXO_MODIFY)) { file.setProperty(NodetypeConstant.EXO_LAST_MODIFIED_DATE, fileDate); file.setProperty(NodetypeConstant.EXO_LAST_MODIFIER, file.getSession().getUserID()); } // Added when upgraded to PLF 4.4 if (!file.isNodeType(NodetypeConstant.MIX_REFERENCEABLE)) { file.addMixin(NodetypeConstant.MIX_REFERENCEABLE); } if (!file.isNodeType(NodetypeConstant.MIX_COMMENTABLE)) { file.addMixin(NodetypeConstant.MIX_COMMENTABLE); } if (!file.isNodeType(NodetypeConstant.MIX_VOTABLE)) { file.addMixin(NodetypeConstant.MIX_VOTABLE); } if (!file.isNodeType(NodetypeConstant.MIX_I18N)) { file.addMixin(NodetypeConstant.MIX_I18N); } return file; } /** * Add nt:folder node with given title. If a node with such name exists and <code>forceNew</code> is * <code>true</code> a new name will be generated by adding a numerical index to the end, otherwise existing * node will be returned. * * @param parent {@link Node} * @param title {@link String} * @param forceNew if <code>true</code> then a new folder will be created with index in suffix, if * <code>false</code> then existing folder will be returned * @return {@link Node} * @throws RepositoryException when storage error */ protected Node addFolder(Node parent, String title, boolean forceNew) throws RepositoryException { Node folder; String baseName = cleanName(title); String name = baseName; int siblingNumber = 0; do { try { folder = parent.getNode(name); if (forceNew) { // such node already exists - find new name for the file (by adding sibling index to the end) siblingNumber++; int extIndex = baseName.lastIndexOf("."); if (extIndex > 0 && extIndex < title.length()) { String jcrName = baseName.substring(0, extIndex); String jcrExt = baseName.substring(extIndex + 1); name = new StringBuilder(jcrName).append('-').append(siblingNumber).append('.') .append(jcrExt).toString(); } else { name = new StringBuilder(baseName).append('-').append(siblingNumber).toString(); } } else { break; } } catch (PathNotFoundException e) { // no such node exists, add it using internalName created by CD's cleanName() folder = parent.addNode(name, "nt:folder"); break; } } while (true); if (folder.isNew()) { if (siblingNumber > 0) { int extIndex = title.lastIndexOf("."); if (extIndex > 0 && extIndex < title.length()) { String titleName = title.substring(0, extIndex); String titleExt = title.substring(extIndex + 1); title = new StringBuilder(titleName).append(" (").append(siblingNumber).append(").") .append(titleExt).toString(); } else { title = new StringBuilder(title).append(" (").append(siblingNumber).append(')').toString(); } } folder.setProperty(NodetypeConstant.EXO_TITLE, title); try { folder.setProperty(NodetypeConstant.EXO_NAME, title); } catch (ConstraintViolationException | ValueFormatException e) { LOG.warn("Cannot set exo:name property to '" + title + "' for folder " + folder.getPath() + ": " + e); } Calendar folderDate = Calendar.getInstance(); if (folder.isNodeType(NodetypeConstant.EXO_DATETIME)) { folder.setProperty(NodetypeConstant.EXO_DATE_CREATED, folderDate); folder.setProperty(NodetypeConstant.EXO_DATE_MODIFIED, folderDate); } if (folder.isNodeType(NodetypeConstant.EXO_MODIFY)) { folder.setProperty(NodetypeConstant.EXO_LAST_MODIFIED_DATE, folderDate); folder.setProperty(NodetypeConstant.EXO_LAST_MODIFIER, folder.getSession().getUserID()); } } return folder; } /** * Current user id. * * @return the string */ protected String currentUserId() { ConversationState contextState = ConversationState.getCurrent(); if (contextState != null) { return contextState.getIdentity().getUserId(); } return IdentityConstants.ANONIM; } /** * Space service. * * @return the space service */ protected SpaceService spaceService() { return (SpaceService) ExoContainerContext.getCurrentContainer() .getComponentInstanceOfType(SpaceService.class); } /** * Social identity manager. * * @return the identity manager */ protected IdentityManager socialIdentityManager() { return (IdentityManager) ExoContainerContext.getCurrentContainer() .getComponentInstanceOfType(IdentityManager.class); } /** * Social activity manager. * * @return the activity manager */ protected ActivityManager socialActivityManager() { return (ActivityManager) ExoContainerContext.getCurrentContainer() .getComponentInstanceOfType(ActivityManager.class); } /** * User spaces. * * @param userId the user id * @return the list * @throws OutlookSpaceException the outlook space exception */ protected List<OutlookSpace> userSpaces(String userId) throws OutlookSpaceException { List<OutlookSpace> spaces = new ArrayList<OutlookSpace>(); ListAccess<Space> list = spaceService().getMemberSpaces(userId); try { for (Space socialSpace : list.load(0, list.getSize())) { spaces.add(new OutlookSpaceImpl(socialSpace)); } return spaces; } catch (Throwable e) { if (LOG.isDebugEnabled()) { LOG.debug("Error loading user spaces", e); } throw new OutlookSpaceException("Error loading user spaces", e); } } /** * Generate the group documents (as /Groups/spaces/$SPACE_GROUP_ID/Documents).<br> * * @param groupId {@link String} * @return {@link String} */ protected String groupDocsPath(String groupId) { // XXX we do here as ECMS does in ManageDriveServiceImpl return GROUP_DRIVE_PARRTEN.replace("${groupId}", groupId); } /** * Generate the user documents (as /Users/${userId}/Private/Documents).<br> * * @param userId {@link String} * @return {@link String} */ protected String userDocsPath(String userId) { // XXX we do here as ECMS does in ManageDriveServiceImpl return PERSONAL_DRIVE_PARRTEN.replace("${userId}", userId) + "/Documents"; } /** * Generate the group node path (as /Groups/spaces/$SPACE_GROUP_ID).<br> * * @param groupId {@link String} * @return {@link String} * @throws Exception when error */ protected String groupPath(String groupId) throws Exception { String groupsPath = hierarchyCreator.getJcrPath(BasePath.CMS_GROUPS_PATH); return groupsPath + groupId; } /** * Inits the web DAV link. * * @param node the node * @throws OutlookException the outlook exception */ protected void initWebDAVLink(HierarchyNode node) throws OutlookException { // WebDAV URL try { node.setWebdavUrl(org.exoplatform.wcm.webui.Utils.getWebdavURL(node.getNode(), false, true)); } catch (Exception e) { throw new OutlookException("Error generating WebDav URL for node " + node.getFullPath(), e); } } /** * Inits the document link. * * @param siteType the site type * @param driveName the drive name * @param portalName the portal name * @param nodeURI the node URI * @param node the node * @throws OutlookException the outlook exception */ protected void initDocumentLink(SiteType siteType, String driveName, String portalName, String nodeURI, HierarchyNode node) throws OutlookException { // WebDAV URL initWebDAVLink(node); // Portal URL // Code adapted from ECMS's PermlinkActionComponent.getPermlink() String npath = node.getPath().replaceAll("/+", "/"); String path = new StringBuilder().append(driveName).append(npath).toString(); PortalRequestContext portalRequest = Util.getPortalRequestContext(); if (portalRequest != null) { NodeURL nodeURL = portalRequest.createURL(NodeURL.TYPE); NavigationResource resource = new NavigationResource(siteType, portalName, nodeURI); nodeURL.setResource(resource); nodeURL.setQueryParameterValue("path", path); HttpServletRequest request = portalRequest.getRequest(); try { URI requestUri = new URI(request.getScheme(), null, request.getServerName(), request.getServerPort(), null, null, null); StringBuilder url = new StringBuilder(); url.append(requestUri.toASCIIString()); url.append(nodeURL.toString()); node.setUrl(url.toString()); } catch (URISyntaxException e) { throw new OutlookException("Error creating server URL " + request.getRequestURI().toString(), e); } } else { LOG.warn("Portal request not found. Node URL will be its WebDAV link. Node: " + node.getPath()); node.setUrl(node.getWebdavUrl()); } } /** * Inits the document link. * * @param space the space * @param file the file * @throws OutlookException the outlook exception */ protected void initDocumentLink(OutlookSpace space, HierarchyNode file) throws OutlookException { // WebDAV URL initWebDAVLink(file); // Portal URL // We need like the following: // https://peter.exoplatform.com.ua:8443/portal/g/:spaces:product_team/product_team/documents?path=.spaces.product_team/Groups/spaces/product_team/Documents/uploads/page_management_https_loading.png initDocumentLink(SiteType.GROUP, // GROUP space.getGroupId().replace("/", "."), space.getGroupId(), // /spaces/product_team space.getShortName() + "/documents", // product_team/documents file); } /** * Inits the document link. * * @param personalDocuments the personal documents * @param file the file * @throws OutlookException the outlook exception */ protected void initDocumentLink(PersonalDocumentsFolder personalDocuments, HierarchyNode file) throws OutlookException { // WebDAV URL initWebDAVLink(file); // Portal URL // We need like the following: // https://peter.exoplatform.com.ua:8443/portal/intranet/documents?path=Personal%20Documents/Users/j___/jo___/joh___/john/Private/Documents initDocumentLink(SiteType.PORTAL, // PORTAL personalDocuments.getDriveName(), "intranet", // intranet "documents", // documents file); } /** * Find given user Personal Documents folder using user session. * * @param userName {@link String} * @return {@link Node} Personal Documents folder node or <code>null</code> * @throws Exception when error */ protected Node userDocumentsNode(String userName) throws Exception { // code idea based on ECMS's UIJCRExplorerPortlet.getUserDrive() for (DriveData userDrive : driveService.getPersonalDrives(userName)) { String homePath = userDrive.getHomePath(); if (homePath.endsWith("/Private")) { String driveRootPath = org.exoplatform.services.cms.impl.Utils.getPersonalDrivePath(homePath, userName); return node(driveRootPath); } } return null; } /** * Find given group Documents folder using current user session. * * @param groupId {@link String} * @return {@link Node} space's Documents folder node or <code>null</code> * @throws Exception when error */ protected Node spaceDocumentsNode(String groupId) throws Exception { return node(groupDocsPath(groupId)); } /** * Set read permissions on the target node to all given identities (e.g. space group members). If node not * yet <code>exo:privilegeable</code> it will add such mixin to allow set the permissions first. Requested * permissions will not be set to the children nodes.<br> * * @param node {@link Node} link target node * @param identities array of {@link String} with user identifiers (names or memberships) * @throws AccessControlException when access error * @throws RepositoryException when storage error */ protected void setPermissions(Node node, String... identities) throws AccessControlException, RepositoryException { setPermissions(node, true, false, identities); } /** * Set read permissions on the target node to all given identities (e.g. space group members). Permissions * will not be set if target not <code>exo:privilegeable</code> and <code>forcePrivilegeable</code> is * <code>false</code>. If <code>deep</code> is <code>true</code> the target children nodes will be checked * also for a need to set the requested permissions. <br> * * @param node {@link Node} link target node * @param deep {@link Boolean} if <code>true</code> then also children nodes will be set to the requested * permissions * @param forcePrivilegeable {@link Boolean} if <code>true</code> and node not yet * <code>exo:privilegeable</code> it will add such mixin to allow set the permissions. * @param identities array of {@link String} with user identifiers (names or memberships) * @throws AccessControlException when access error * @throws RepositoryException when storage error */ protected void setPermissions(Node node, boolean deep, boolean forcePrivilegeable, String... identities) throws AccessControlException, RepositoryException { ExtendedNode target = (ExtendedNode) node; boolean setPermissions = true; if (target.canAddMixin(EXO_PRIVILEGEABLE)) { if (forcePrivilegeable) { target.addMixin(EXO_PRIVILEGEABLE); } else { // will not set permissions on this node, but will check the child nodes setPermissions = false; } } // else, already exo:privilegeable if (setPermissions) { for (String identity : identities) { String[] ids = identity.split(":"); if (ids.length == 2) { // it's group and we want allow given identity read only and additionally let managers remove the // link String managerMembership; try { MembershipType managerType = organization.getMembershipTypeHandler() .findMembershipType("manager"); managerMembership = managerType.getName(); } catch (Exception e) { LOG.error("Error finding manager membership in organization service. " + "Will use any (*) to allow remove shared cloud file link", e); managerMembership = "*"; } target.setPermission(new StringBuilder(managerMembership).append(':').append(ids[1]).toString(), MANAGER_PERMISSION); target.setPermission(identity, READER_PERMISSION); } else { // in other cases, we assume it's user identity and user should be able to remove the node target.setPermission(identity, MANAGER_PERMISSION); } } } if (deep) { // check the all children also, but don't force adding exo:privilegeable for (NodeIterator niter = target.getNodes(); niter.hasNext();) { Node child = niter.nextNode(); setPermissions(child, true, false, identities); } } } /** * Messages folder. * * @param parent the parent * @param identity the identity * @return the node * @throws RepositoryException the repository exception */ protected Node messagesFolder(Node parent, String... identity) throws RepositoryException { Node messagesFolder; if (!parent.hasNode("outlook-messages")) { messagesFolder = parent.addNode(OUTLOOK_MESSAGES_NAME, "nt:folder"); messagesFolder.setProperty("exo:title", OUTLOOK_MESSAGES_TITLE); try { messagesFolder.setProperty("exo:name", OUTLOOK_MESSAGES_TITLE); } catch (ConstraintViolationException | ValueFormatException e) { LOG.warn("Cannot set exo:name property for folder " + messagesFolder.getPath() + ": " + e); } if (identity != null) { setPermissions(messagesFolder, identity); } parent.save(); } else { messagesFolder = parent.getNode(OUTLOOK_MESSAGES_NAME); } return messagesFolder; } /** * Adds the message file. * * @param parent the parent * @param message the message * @return the node * @throws RepositoryException the repository exception * @throws UnsupportedEncodingException the unsupported encoding exception * @throws IOException Signals that an I/O exception has occurred. */ protected Node addMessageFile(Node parent, OutlookMessage message) throws RepositoryException, UnsupportedEncodingException, IOException { String safeTitle = safeText(message.getSubject()); String safeContent = safeHtml(message.getBody()); try (InputStream content = new ByteArrayInputStream(safeContent.getBytes("UTF-8"))) { // message file goes w/o summary, it will be generated in UI (OutlookMessageActivity) Node messageFile = addFile(parent, safeTitle, "text/html", content); messageFile.addMixin(MESSAGE_NODETYPE); messageFile.setProperty("mso:userEmail", message.getUser().getEmail()); messageFile.setProperty("mso:userName", message.getUser().getDisplayName()); messageFile.setProperty("mso:fromEmail", message.getFrom().getEmail()); messageFile.setProperty("mso:fromName", message.getFrom().getDisplayName()); // messageFile.setProperty("mso:toEmail", userEmail); messageFile.setProperty("mso:created", message.getCreated()); messageFile.setProperty("mso:modified", message.getModified()); messageFile.setProperty("mso:messageId", message.getId()); return messageFile; } } /** * Post attachment activity. * * @param destFolder the dest folder * @param files the files * @param user the user * @param comment the comment * @return the exo social activity * @throws RepositoryException the repository exception */ protected ExoSocialActivity postAttachmentActivity(Folder destFolder, List<File> files, OutlookUser user, String comment) throws RepositoryException { String author = user.getLocalUser(); // FYI Code inspired by UIDocActivityComposer Map<String, String> activityParams = new LinkedHashMap<String, String>(); StringBuilder filesLine = new StringBuilder(); for (File f : files) { if (filesLine.length() > 0) { filesLine.append(','); } filesLine.append(OutlookAttachmentActivity.attachmentString(f.getNode().getUUID(), f.getTitle())); } activityParams.put(OutlookAttachmentActivity.FILES, filesLine.toString()); activityParams.put(OutlookAttachmentActivity.WORKSPACE, destFolder.getNode().getSession().getWorkspace().getName()); activityParams.put(OutlookAttachmentActivity.COMMENT, comment); activityParams.put(OutlookAttachmentActivity.AUTHOR, author); Calendar activityDate = Calendar.getInstance(); DateFormat dateFormatter = new SimpleDateFormat(ISO8601.SIMPLE_DATETIME_FORMAT); String dateString = dateFormatter.format(activityDate.getTime()); activityParams.put(OutlookAttachmentActivity.DATE_CREATED, dateString); activityParams.put(OutlookAttachmentActivity.DATE_LAST_MODIFIED, dateString); // if NT_FILE // activityParams.put(UIDocActivity.ID, node.isNodeType(NodetypeConstant.MIX_REFERENCEABLE) ? // node.getUUID() : ""); // activityParams.put(UIDocActivity.CONTENT_NAME, node.getName()); // activityParams.put(UIDocActivity.AUTHOR, activityOwnerId); // activityParams.put(UIDocActivity.DATE_CREATED, strDateCreated); // activityParams.put(UIDocActivity.LAST_MODIFIED, strLastModified); // activityParams.put(UIDocActivity.CONTENT_LINK, UIDocActivity.getContentLink(node)); // IdentityManager identityManager = socialIdentityManager(); Identity authorIdentity = identityManager.getOrCreateIdentity(OrganizationIdentityProvider.NAME, author, true); // String title = comment != null && comment.length() > 0 ? comment : new StringBuilder("User ").append(author).append(" has saved ").append(files.size()) .append(files.size() > 1 ? " files" : " file").toString(); ExoSocialActivity activity = new ExoSocialActivityImpl(authorIdentity.getId(), OutlookAttachmentActivity.ACTIVITY_TYPE, title, null); activity.setTemplateParams(activityParams); // activity destination (user or space) ActivityManager activityManager = socialActivityManager(); String spaceGroupName = getSpaceName(destFolder.getNode()); Space space = spaceService().getSpaceByGroupId(SpaceUtils.SPACE_GROUP + "/" + spaceGroupName); if (spaceGroupName != null && spaceGroupName.length() > 0 && space != null) { // post activity to space stream Identity spaceIdentity = identityManager.getOrCreateIdentity(SpaceIdentityProvider.NAME, space.getPrettyName(), true); activityManager.saveActivityNoReturn(spaceIdentity, activity); } else { // post activity to user status stream activityManager.saveActivityNoReturn(authorIdentity, activity); } // activity = activityManager.getActivity(activity.getId()); return activity; } /** * Read email. * * @param vElem the v elem * @return the outlook email * @throws OutlookException the outlook exception */ protected OutlookEmail readEmail(JsonValue vElem) throws OutlookException { JsonValue vEmailAddress = vElem.getElement("EmailAddress"); if (isNull(vEmailAddress)) { throw new OutlookFormatException("Element doesn't contain EmailAddress"); } String email; JsonValue vAddress = vEmailAddress.getElement("Address"); if (isNull(vAddress)) { throw new OutlookFormatException("Element doesn't contain Address"); } else { email = vAddress.getStringValue(); } String name; JsonValue vName = vEmailAddress.getElement("Name"); if (isNull(vName)) { name = "".intern(); } else { name = vName.getStringValue(); } return getAddress(email, name); } /** * Fetch query. * * @param qr the qr * @param limit the limit * @param res the res * @throws RepositoryException the repository exception * @throws OutlookException the outlook exception */ protected void fetchQuery(QueryResult qr, int limit, Set<File> res) throws RepositoryException, OutlookException { fetchQuery(qr, limit, res, n -> true); } /** * Fetch query. * * @param qr the qr * @param limit the limit * @param res the res * @param acceptNode the accept node * @throws RepositoryException the repository exception * @throws OutlookException the outlook exception */ protected void fetchQuery(QueryResult qr, int limit, Set<File> res, Predicate<Node> acceptNode) throws RepositoryException, OutlookException { SpaceService spaceService = spaceService(); for (NodeIterator niter = qr.getNodes(); niter.getPosition() < limit && niter.hasNext();) { Node node = niter.nextNode(); try { if (acceptNode.test(node)) { // detect is it space and then check if space member String path = node.getPath(); Space space; if (path.startsWith(SPACES_HOME)) { try { String groupId = path.substring(7, path.indexOf("/", SPACES_HOME.length() + 1)); space = spaceService.getSpaceByGroupId(groupId); if (space != null) { Set<String> allMemembers = new HashSet<String>(); for (String s : space.getManagers()) { allMemembers.add(s); } for (String s : space.getMembers()) { allMemembers.add(s); } if (!allMemembers.contains(currentUserId())) { // when not a space member - skip this file (but user still may be an owner of it!) limit++; continue; } } } catch (IndexOutOfBoundsException e) { // XXX something not clear with space path, will use portal page path as for Personal // Documents (it works well in PLF 4.3) space = null; } } else { space = null; } UserFile file = new UserFile(node.getParent().getPath(), node); if (space != null) { initDocumentLink(SiteType.GROUP, // GROUP space.getGroupId().replace("/", "."), space.getGroupId(), // /spaces/product_team space.getShortName() + "/documents", // product_team/documents file); } else { initDocumentLink(SiteType.PORTAL, // PORTAL PERSONAL_DOCUMENTS, "intranet", // intranet "documents", // documents file); } res.add(file); } else { limit++; } } catch (RepositoryException e) { LOG.warn("Error read queried node " + e.getMessage() + ". Node skipped: " + node); limit++; } } } /** * Method adapted from eXo Chat's WikiService.createOrEditPage(). * * @param wikiType {@link String} * @param wikiOwner {@link String} * @param creator {@link String} * @param title {@link String} * @param summary {@link String} * @param content {@link String} * @param users list of {@link String} * @return {@link Page} * @throws Exception when error */ protected Page createWikiPage(String wikiType, String wikiOwner, String creator, String title, String summary, String content, List<String> users) throws Exception { final String parentTitle = OUTLOOK_MESSAGES_TITLE; final String parentId = TitleResolver.getId(parentTitle, false); final String defaultSyntax = wikiService.getDefaultWikiSyntaxId(); // Syntax.XWIKI_2_0.toIdString() final String xhtmlSyntax = Syntax.XHTML_1_0.toIdString(); synchronized (wikiService) { Page parentPage = wikiService.getPageOfWikiByName(wikiType, wikiOwner, parentId); if (parentPage == null) { parentPage = new Page(); parentPage.setTitle(parentTitle); parentPage.setContent("= " + parentTitle + " =\n"); parentPage.setSyntax(defaultSyntax); Wiki wiki = wikiService.getWikiByTypeAndOwner(wikiType, wikiOwner); if (wiki == null) { wiki = wikiService.createWiki(wikiType, wikiOwner); } Page wikiHome = wiki.getWikiHome(); setPermissionForWikiPage(Collections.<String>emptyList(), parentPage, wikiHome); List<PermissionEntry> permissions = parentPage.getPermissions(); permissions.add(new PermissionEntry(WIKI_PERMISSION_ANY, "", IDType.USER, new Permission[] { new Permission(org.exoplatform.wiki.mow.api.PermissionType.VIEWPAGE, true) })); parentPage.setPermissions(permissions); Wiki pwiki = new Wiki(); pwiki.setOwner(wikiOwner); pwiki.setType(wikiType); parentPage = wikiService.createPage(pwiki, "WikiHome", parentPage); } Wiki wiki = new Wiki(); wiki.setOwner(wikiOwner); wiki.setType(wikiType); Page page = new Page(); if (isHTML(content) && !defaultSyntax.equals(xhtmlSyntax)) { // we use xWiki syntax for a page and convert incoming HTML to xWiki format page.setSyntax(defaultSyntax); StringBuilder quotedContent = new StringBuilder(); if (summary != null) { // page also contains message summary (US_003_07) quotedContent.append("(% style='word-wrap: break-word; min-height: 30px;' %)(((\r"); String xwikiMarkup = wikiRenderingService.render(summary, xhtmlSyntax, defaultSyntax, false); quotedContent.append(xwikiMarkup); quotedContent.append("\r)))\r"); } // message content should look like a quoted in email client (vertical gray bar on the left) // in xWiki syntax it's a table with customized style // table header: quotedContent.append("|=(% style='background-color: #999999; border-style: hidden;' %)"); quotedContent.append("|=(% style='background-color: inherit; border-style: hidden;' %)"); quotedContent.append("|=(% style='background-color: inherit; border-style: hidden;' %)\r"); // table row: quotedContent.append("|(% style='background-color: #999999; border-style: hidden;' %) "); quotedContent.append("|(% style='border-style: hidden;' %) "); quotedContent.append("|(% style='border-style: hidden;' %) (((\r"); String xwikiMarkup = wikiRenderingService.render(safeHtml(content), xhtmlSyntax, defaultSyntax, false); quotedContent.append(xwikiMarkup); quotedContent.append("\r)))\r"); page.setContent(quotedContent.toString()); } else { page.setSyntax(defaultSyntax); page.setContent(content); } setPermissionForWikiPage(users, page, parentPage); page.setOwner(creator); page.setAuthor(creator); page.setMinorEdit(false); // title = safeText(title); String baseTitle = title; int siblingNumber = 0; do { String pageId = TitleResolver.getId(title, false); page.setTitle(title); String path = ""; if (wikiType.equals(PortalConfig.GROUP_TYPE)) { // http://demo.exoplatform.net/portal/intranet/wiki/group/spaces/bank_project/Meeting_06-11-2013 path = "/portal/intranet/wiki/" + wikiType + wikiOwner + "/" + pageId; } else if (wikiType.equals(PortalConfig.PORTAL_TYPE)) { // http://demo.exoplatform.net/portal/intranet/wiki/Sales_Meetings_Meeting_06-11-2013 path = "/portal/intranet/wiki/" + pageId; } page.setUrl(path); if (!wikiService.isExisting(wikiType, wikiOwner, pageId)) { try { page = wikiService.createPage(wiki, parentId, page); break; } catch (WikiException e) { LOG.warn("Error creating wiki page " + title + " (" + pageId + "). " + e.getMessage()); try { wikiService.getPageById(pageId); } catch (WikiException ge) { // if we caught error here - we thrown a first one of the creation throw e; } } } // such page already exists - find new name for it (by adding sibling index to the end) siblingNumber++; title = new StringBuilder(baseTitle).append(" (").append(siblingNumber).append(')').toString(); } while (true); return page; } } /** * Generate message summary text. * * @param message {@link String} * @return {@link String} */ protected String messageSummary(OutlookMessage message) { String fromEmail = message.getFrom().getEmail(); String fromName = message.getFrom().getDisplayName(); Date time = message.getCreated().getTime(); Locale locale = Locale.ENGLISH; ResourceBundle res = resourceBundleService.getResourceBundle("locale.outlook.Outlook", locale); DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.FULL, locale); DateFormat timeFormat = DateFormat.getTimeInstance(DateFormat.SHORT, locale); StringBuilder fromLine = new StringBuilder(); fromLine.append(fromName); fromLine.append('<'); fromLine.append(fromEmail); fromLine.append('>'); StringBuilder summary = new StringBuilder(); summary.append(res.getString("Outlook.activity.from")); summary.append(": <a href='mailto:"); summary.append(fromEmail); summary.append("' target='_top'>"); summary.append(ContentReader.simpleEscapeHtml(fromLine.toString())); summary.append("</a> "); summary.append(res.getString("Outlook.activity.on")); summary.append(' '); summary.append(dateFormat.format(time)); summary.append(' '); summary.append(res.getString("Outlook.activity.at")); summary.append(' '); summary.append(timeFormat.format(time)); return summary.toString(); } /** * Try detect is it a HTML content in the string. * * @param content {@link String} * @return boolean */ protected boolean isHTML(String content) { // XXX well, it's not proper, but working in most of cases approach int istart = content.indexOf("<html"); int iend = content.indexOf("</html>"); if (istart >= 0 && iend > 0 && istart < iend) { return true; } istart = content.indexOf("<body"); iend = content.indexOf("</body>"); if (istart >= 0 && iend > 0 && istart < iend) { return true; } istart = content.indexOf("<div"); iend = content.indexOf("</div>"); if (istart >= 0 && iend > 0 && istart < iend) { return true; } // FYI it's how looks message after MS Word pre-peocessor in Outlook for Windows: // everything in tables (no html, body or divs) istart = content.indexOf("<table"); iend = content.indexOf("</table>"); if (istart >= 0 && iend > 0 && istart < iend) { return true; } istart = content.indexOf("<style"); iend = content.indexOf("</style>"); if (istart >= 0 && iend > 0 && istart < iend) { return true; } return false; } /** * Allow full rage of HTML text and structures. * * @param content {@link String} * @return {@link String} sanitized content */ protected String safeHtml(String content) { String safe = htmlPolicy.sanitize(content); safe = makeLinksOpenNewWindow(safe); return safe; } /** * Allow only plain text. * * @param content {@link String} * @return {@link String} sanitized content (as plain text) */ protected String safeText(String content) { String safe = textPolicy.sanitize(content); safe = makeLinksOpenNewWindow(safe); safe = StringEscapeUtils.unescapeHtml(safe); return safe; } /** * Allow only activity tags as described in * https://www.exoplatform.com/docs/PLF43/PLFUserGuide.GettingStarted.ActivitiesInActivityStream.HTMLTags. * html. * * @param text {@link String} * @return {@link String} allowed content */ protected String safeActivityMessage(String text) { String safe = activityPolicy.sanitize(text); safe = makeLinksOpenNewWindow(safe); safe = StringEscapeUtils.unescapeHtml(safe); return safe; } /** * Make links open new window. * * @param text the text * @return the string */ protected String makeLinksOpenNewWindow(String text) { // Make all links target a new window // Replace in all links with target attribute to its _blank value Matcher m = linkWithTarget.matcher(text); StringBuilder sb = new StringBuilder(); int pos = 0; while (m.find()) { if (linkNotLocal.matcher(m.group()).find()) { int start = m.start(1); int end = m.end(1); if (start >= 0 && end >= 0) { sb.append(text.substring(pos, start)); sb.append("target=\"_blank\""); pos = end; } else { if (LOG.isDebugEnabled()) { LOG.debug("Cannot find link target group in " + m.group(1)); } } } } if (pos < text.length()) { sb.append(text.substring(pos)); } text = sb.toString(); // Add in all links without target attribute add it with _blank value m = linkWithoutTarget.matcher(text); sb = new StringBuilder(); pos = 0; while (m.find()) { if (linkNotLocal.matcher(m.group()).find()) { int start = m.start(2); int end = m.end(2); if (start >= 0 && end >= 0) { sb.append(text.substring(pos, start)); sb.append(" target=\"_blank\""); sb.append(text.substring(start, end)); pos = end; } else { if (LOG.isDebugEnabled()) { LOG.debug("Cannot find link end group in " + m.group(2)); } } } } if (pos < text.length()) { sb.append(text.substring(pos)); } return sb.toString(); } /** * Method adapted from eXo Chat's WikiService.setPermissionForReportAsWiki(). * * @param users list of {@link String} * @param page {@link Page} * @param parentPage {@link Page} */ protected void setPermissionForWikiPage(List<String> users, Page page, Page parentPage) { Permission[] allPermissions = new Permission[] { new Permission(org.exoplatform.wiki.mow.api.PermissionType.VIEWPAGE, true), new Permission(org.exoplatform.wiki.mow.api.PermissionType.EDITPAGE, true), }; List<PermissionEntry> permissions = parentPage.getPermissions(); if (permissions != null) { // remove any permission int anyIndex = -1; for (int i = 0; i < permissions.size(); i++) { PermissionEntry any = permissions.get(i); if (WIKI_PERMISSION_ANY.equals(any.getId())) anyIndex = i; } if (anyIndex > -1) { permissions.remove(anyIndex); } for (int i = 0; i < users.size(); i++) { String strUser = users.get(i).toString(); PermissionEntry userPermission = new PermissionEntry(strUser, strUser, IDType.USER, allPermissions); permissions.add(userPermission); } page.setPermissions(permissions); } } /** * Method inspired by code of UIPostForm. * * @param categoryId {@link String} * @param forumId {@link String} * @param creator {@link String} * @param title {@link String} * @param summary {@link String} * @param content {@link String} * @return {@link Topic} created topic * @throws Exception when error */ protected Topic createForumTopic(String categoryId, String forumId, String creator, String title, String summary, String content) throws Exception { // save topic in ForumService org.exoplatform.forum.service.UserProfile userProfile = forumService.getUserSettingProfile(creator); if (checkForumHasAddTopic(userProfile, categoryId, forumId)) { Topic topic = new Topic(); String message; if (isHTML(content)) { message = safeHtml(content); } else { message = content; } String safeTitle = safeText(title); // check if title not empty if (safeTitle.length() <= 0 || safeTitle.equals("null")) { if (summary != null) { safeTitle = safeText(summary); } else { safeTitle = safeText(message.substring(0, ForumUtils.MAXTITLE - 5) + "..."); } } if (safeTitle.length() > ForumUtils.MAXTITLE) { // TODO throw an exception to user to ask for shorten title safeTitle = new StringBuilder(safeTitle.substring(0, ForumUtils.MAXTITLE - 3)).append("...") .toString(); } String checksms = TransformHTML.cleanHtmlCode(message, new ArrayList<String>((new ExtendedBBCodeProvider()).getSupportedBBCodes())); checksms = checksms.replaceAll(" ", " "); int t = checksms.trim().length(); if (t > 0 && !checksms.equals("null")) { // TODO in else block handle too short or offending texts } Date currentDate = CommonUtils.getGreenwichMeanTime().getTime(); message = CommonUtils.encodeSpecialCharInSearchTerm(message); message = TransformHTML.fixAddBBcodeAction(message); // TODO do we need this when using safe HTML? // message = message.replaceAll("<script", "<script").replaceAll("<link", // "<link").replaceAll("</script>", // "</script>"); // remove any meta tags explicitly existing in the content // message = message.replaceAll("<meta.*?>", ""); // remove all embedded global styles // message = message.replaceAll("<style.*?>[.\\s\\w\\W]*?<\\/style>", ""); boolean isOffend = false; ForumAdministration forumAdministration = forumService.getForumAdministration(); String[] censoredKeyword = ForumUtils.getCensoredKeyword(forumAdministration.getCensoredKeyword()); checksms = checksms.toLowerCase(); for (String string : censoredKeyword) { if (checksms.indexOf(string.trim()) >= 0) { isOffend = true; break; } if (safeTitle.toLowerCase().indexOf(string.trim()) >= 0) { isOffend = true; break; } } boolean topicClosed = false; // uiForm.getUIForumCheckBoxInput(FIELD_TOPICSTATE_SELECTBOX).isChecked(); boolean topicLocked = false; // uiForm.getUIForumCheckBoxInput(FIELD_TOPICSTATUS_SELECTBOX).isChecked(); boolean sticky = false; // uiForm.getUIForumCheckBoxInput(FIELD_STICKY_CHECKBOX).isChecked(); boolean moderatePost = true; // uiForm.getUIForumCheckBoxInput(FIELD_MODERATEPOST_CHECKBOX).isChecked(); boolean whenNewPost = true; // uiForm.getUIForumCheckBoxInput(FIELD_NOTIFYWHENADDPOST_CHECKBOX).isChecked(); // TODO permissions? // UIPermissionPanel permissionTab = uiForm.getChildById(PERMISSION_TAB); String canPost = ForumUtils.EMPTY_STR; // permissionTab.getOwnersByPermission(CANPOST); String canView = ForumUtils.EMPTY_STR; // permissionTab.getOwnersByPermission(CANVIEW); // set link // FYI this original Forum code will use current "outlook" portlet path to build the link // ForumUtils.createdForumLink(ForumUtils.TOPIC, topic.getId(), false) String link = BuildLinkUtils.buildLink(forumId, topic.getId(), PORTLET_INFO.FORUM); // finally escape the title safeTitle = CommonUtils.encodeSpecialCharInTitle(safeTitle); topic.setTopicName(safeTitle); topic.setModifiedBy(creator); topic.setModifiedDate(currentDate); if (summary != null) { // if summary given then we assume need quote the message content StringBuilder topicContent = new StringBuilder(); if (summary != null) { // HTML also contains message summary topicContent.append( "<div class='messageSummary' style='word-wrap: break-word; min-height: 30px;'>"); topicContent.append(summary); topicContent.append("</div>"); } topicContent.append( "<div class='messageQuote' style='overflow:auto;'><div class='messageContent' style='position: relative; float: left;" + "box-sizing: border-box; padding-left: 7px; min-width: 100%; max-height: 100%;" + "border-width: 0px 0px 0px 12px; border-style: solid; border-color: #999999; background-color: white;'>"); topicContent.append(message); topicContent.append("</div></div>"); message = topicContent.toString(); } // otherwise message content will be a content of the topic post topic.setDescription(message); topic.setLink(link); if (whenNewPost) { String email = userProfile.getEmail(); if (email == null || email.length() <= 0) { try { email = organization.getUserHandler().findUserByName(creator).getEmail(); } catch (Exception e) { email = "true"; } } topic.setIsNotifyWhenAddPost(email); } else { topic.setIsNotifyWhenAddPost(ForumUtils.EMPTY_STR); } topic.setIsWaiting(isOffend); topic.setIsClosed(topicClosed); topic.setIsLock(topicLocked); topic.setIsModeratePost(moderatePost); topic.setIsSticky(sticky); topic.setIcon("uiIconForumTopic uiIconForumLightGray"); String[] canPosts = ForumUtils.splitForForum(canPost); String[] canViews = ForumUtils.splitForForum(canView); topic.setCanView(canViews); topic.setCanPost(canPosts); topic.setIsApproved(true); // !hasForumMod MessageBuilder messageBuilder = new MessageBuilder(); WebuiRequestContext context = WebuiRequestContext.getCurrentInstance(); ResourceBundle res = resourceBundleService.getResourceBundle("locale.portlet.forum.ForumPortlet", context.getLocale()); if (res != null) { // TODO this will not work as resources aren't reachable here - it's DEAD CODE in fact try { messageBuilder.setContent(res.getString("UINotificationForm.label.notifyEmailContentDefault")); String header = res.getString("UINotificationForm.label.notifyEmailHeaderSubjectDefault"); messageBuilder.setHeaderSubject( header == null || header.trim().length() == 0 ? ForumUtils.EMPTY_STR : header); messageBuilder.setTypes(res.getString("UIForumPortlet.label.category"), res.getString("UIForumPortlet.label.forum"), res.getString("UIForumPortlet.label.topic"), res.getString("UIForumPortlet.label.post")); } catch (Exception e) { if (LOG.isDebugEnabled()) { LOG.debug("Failed to get resource bundle for Forum default content email notification", e); } } } else { if (LOG.isDebugEnabled()) { LOG.debug( "Locale resource bundle cannot be found for Forum default content email notification"); } } messageBuilder.setLink(link); topic.setOwner(creator); topic.setCreatedDate(currentDate); topic.setLastPostBy(creator); topic.setLastPostDate(currentDate); topic.setVoteRating(0.0); topic.setUserVoteRating(new String[] {}); try { String remoteAddr = ForumUtils.EMPTY_STR; remoteAddr = WebUIUtils.getRemoteIP(); topic.setRemoteAddr(remoteAddr); forumService.saveTopic(categoryId, forumId, topic, true, false, messageBuilder); if (userProfile.getIsAutoWatchMyTopics()) { List<String> values = new ArrayList<String>(); values.add(userProfile.getEmail()); String path = new StringBuilder(categoryId).append(ForumUtils.SLASH).append(forumId) .append(ForumUtils.SLASH).append(topic.getId()).toString(); forumService.addWatch(1, path, values, creator); } } catch (PathNotFoundException e) { throw new OutlookException("Error saving forum topic '" + title + "'", e); } return topic; } else { throw new BadParameterException("Cannot add forum topic. Check user permissions or forum settings."); } } /** * Check forum has add topic. * * @param userProfile the user profile * @param categoryId the category id * @param forumId the forum id * @return true, if successful * @throws Exception the exception */ protected boolean checkForumHasAddTopic(org.exoplatform.forum.service.UserProfile userProfile, String categoryId, String forumId) throws Exception { // FYI Adapted code from UIForumPortlet. // is guest or banned if (userProfile.getUserRole() == org.exoplatform.forum.service.UserProfile.GUEST || userProfile.getIsBanned() || userProfile.isDisabled()) { return false; } try { Category cate = forumService.getCategory(categoryId); Forum forum = forumService.getForum(categoryId, forumId); if (forum == null) { return false; } // forum close or lock if (forum.getIsClosed() || forum.getIsLock()) { return false; } // isAdmin if (userProfile.getUserRole() == 0) { return true; } // is moderator if (userProfile.getUserRole() == 1) { String[] morderators = ForumUtils.arraysMerge(cate.getModerators(), forum.getModerators()); // if (ForumServiceUtils.isModerator(morderators, userProfile.getUserId())) { return true; } } // FYI it's possible to ban IP of forum // check access category if (!ForumServiceUtils.hasPermission(cate.getUserPrivate(), userProfile.getUserId())) { return false; } // can add topic on category/forum String[] canCreadTopic = ForumUtils.arraysMerge(forum.getCreateTopicRole(), cate.getCreateTopicRole()); if (!ForumServiceUtils.hasPermission(canCreadTopic, userProfile.getUserId())) { return false; } } catch (Exception e) { LOG.warn(String.format("Check permission to add topic of category %s, forum %s unsuccessfully.", categoryId, forumId)); if (LOG.isDebugEnabled()) { LOG.debug(e); } return false; } return true; } /** * Get the space name of node. * * @param node {@link Node} * @return {@link String} the group name * @throws RepositoryException when storage error */ private static String getSpaceName(Node node) throws RepositoryException { NodeHierarchyCreator nodeHierarchyCreator = (NodeHierarchyCreator) ExoContainerContext.getCurrentContainer() .getComponentInstanceOfType(NodeHierarchyCreator.class); String groupPath = nodeHierarchyCreator.getJcrPath(BasePath.CMS_GROUPS_PATH); String spacesFolder = groupPath + "/spaces/"; String spaceName = ""; String nodePath = node.getPath(); if (nodePath.startsWith(spacesFolder)) { spaceName = nodePath.substring(spacesFolder.length()); spaceName = spaceName.substring(0, spaceName.indexOf("/")); } return spaceName; } /** * Make JCR compatible item name. * * @param name {@link String} * @return {@link String} JCR compatible name of local file */ public static String cleanName(String name) { String str = accentsConverter.transliterate(name.trim()); // the character ? seems to not be changed to d by the transliterate function StringBuilder cleanedStr = new StringBuilder(str.trim()); // delete special character if (cleanedStr.length() == 1) { char c = cleanedStr.charAt(0); if (c == '.' || c == '/' || c == ':' || c == '[' || c == ']' || c == '*' || c == '\'' || c == '"' || c == '|') { // any -> _<NEXNUM OF c> cleanedStr.deleteCharAt(0); cleanedStr.append('_'); cleanedStr.append(Integer.toHexString(c).toUpperCase()); } } else { for (int i = 0; i < cleanedStr.length(); i++) { char c = cleanedStr.charAt(i); if (c == '/' || c == ':' || c == '[' || c == ']' || c == '*' || c == '\'' || c == '"' || c == '|') { cleanedStr.deleteCharAt(i); cleanedStr.insert(i, '_'); } else if (!(Character.isLetterOrDigit(c) || Character.isWhitespace(c) || c == '.' || c == '-' || c == '_')) { cleanedStr.deleteCharAt(i--); } } // XXX finally ensure the name doesn't hava a dot at the end // https://github.com/exo-addons/outlook/issues/5 // https://jira.exoplatform.org/browse/COMMONS-510 int lastCharIndex = cleanedStr.length() - 1; char c = cleanedStr.charAt(lastCharIndex); if (c == '.') { cleanedStr.deleteCharAt(lastCharIndex); } } return cleanedStr.toString().trim(); // finally trim also } }