All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.adobe.cq.social.commons.comments.endpoints.AbstractCommentOperationService Maven / Gradle / Ivy

/*************************************************************************
 *
 * ADOBE CONFIDENTIAL
 * __________________
 *
 *  Copyright 2013 Adobe Systems Incorporated
 *  All Rights Reserved.
 *
 * NOTICE:  All information contained herein is, and remains
 * the property of Adobe Systems Incorporated and its suppliers,
 * if any.  The intellectual and technical concepts contained
 * herein are proprietary to Adobe Systems Incorporated and its
 * suppliers and are protected by trade secret or copyright law.
 * Dissemination of this information or reproduction of this material
 * is strictly forbidden unless prior written permission is obtained
 * from Adobe Systems Incorporated.
 **************************************************************************/
package com.adobe.cq.social.commons.comments.endpoints;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.AccessControlException;
import java.text.Normalizer;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.activation.DataSource;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.Value;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.collections.IteratorUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Properties;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferenceCardinality;
import org.apache.felix.scr.annotations.ReferencePolicy;
import org.apache.jackrabbit.api.security.user.Authorizable;
import org.apache.jackrabbit.api.security.user.UserManager;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.request.RequestParameter;
import org.apache.sling.api.request.RequestParameterMap;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.ModifiableValueMap;
import org.apache.sling.api.resource.PersistenceException;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.commons.osgi.OsgiUtil;
import org.apache.sling.jcr.api.SlingRepository;
import org.apache.sling.jcr.resource.JcrResourceConstants;
import org.apache.sling.settings.SlingSettingsService;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.event.EventAdmin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.adobe.cq.social.commons.CollabUser;
import com.adobe.cq.social.commons.CollabUtil;
import com.adobe.cq.social.commons.Comment;
import com.adobe.cq.social.commons.CommentException;
import com.adobe.cq.social.commons.CommentSystem;
import com.adobe.cq.social.commons.CommentUtil;
import com.adobe.cq.social.commons.FileUploadSizeLimit;
import com.adobe.cq.social.commons.bundleactivator.Activator;
import com.adobe.cq.social.commons.comments.scheduler.api.ScheduledPostService;
import com.adobe.cq.social.commons.comments.states.internal.State;
import com.adobe.cq.social.commons.comments.states.internal.StateMachine;
import com.adobe.cq.social.commons.events.CommentEvent;
import com.adobe.cq.social.commons.ugclimiter.api.UGCLimiterService;
import com.adobe.cq.social.scf.InheritedOperationExtensionManager;
import com.adobe.cq.social.scf.Operation;
import com.adobe.cq.social.scf.OperationException;
import com.adobe.cq.social.scf.OperationExtension;
import com.adobe.cq.social.scf.SocialComponent;
import com.adobe.cq.social.scf.SocialComponentFactoryManager;
import com.adobe.cq.social.scf.core.SocialEvent;
import com.adobe.cq.social.scf.core.operations.AbstractOperationService;
import com.adobe.cq.social.srp.SocialResourceProvider;
import com.adobe.cq.social.ugcbase.SocialUtils;
import com.adobe.cq.social.ugcbase.core.SocialResourceUtils;
import com.adobe.cq.social.ugcbase.core.attachments.AttachmentUtils;
import com.adobe.cq.social.graph.SocialGraph;
import com.adobe.granite.socialgraph.Direction;
import com.adobe.granite.socialgraph.Relationship;
import com.adobe.cq.social.graph.Vertex;
import com.adobe.granite.security.user.UserProperties;
import com.day.cq.commons.Externalizer;
import com.day.cq.commons.date.DateUtil;
import com.day.cq.commons.date.InvalidDateException;
import com.day.cq.wcm.api.NameConstants;
import com.day.text.Text;

/**
 * Provides abstract implementation of comment operations. This class can be extended to implement
 * create/delete/update operations for any component that extends the comment system.
 * @param  is a {@link OperationExtension} that will be used as hooks by the extending class.
 * @param  is a {@link Operation} that is being provided by the extending class.
 */
@Component(metatype = false, componentAbstract = true)
@Properties({
    @Property(name = AbstractCommentOperationService.PROPERTY_FIELD_WHITELIST, value = {"cq:tags", "tags",
        "composedFor"}, cardinality = 100),
    /*
     * NOTE: the single value DEFAULT array here is special and activates the new service to provide the value by
     * default, but allows existing configurations to override the value.
     */
    @Property(name = AbstractCommentOperationService.PROPERTY_ATTACHMENT_TYPE_BLACKLIST,
            cardinality = Integer.MAX_VALUE, value = {"DEFAULT"})})
public abstract class AbstractCommentOperationService
    extends AbstractOperationService {
    private static final String CQ_TAGS_PROPERTY = "cq:tags";

    /** The Constant CHARSET_PROPERTY. */
    public static final String CHARSET_PROPERTY = "_charset_";

    /** The Constant TAGS_PROPERTY. */
    public static final String TAGS_PROPERTY = "tags";

    /**
     * The name of the property that holds a white list of form field names added as additional properties to the
     * comment.
     */
    public static final String PROPERTY_FIELD_WHITELIST = "fieldWhitelist";
    private static final String MODERATORS_ROLE = "moderators";
    private static final String OWNER_ROLE = "owner";
    private static final String PROPERTY_STATE = "state_s";
    private static final String PROPERTY_TOSTATE = "toState";
    private static final String PROPERTY_ISCREATEDBYPRIVUSER = "isCreatedByPrivilegedUser";
    private static final String PROPERTY_STATE_OPERATION = "stateOperation";
    private static final String PROPERTY_ADD_REPLY = "addReply";
    private static final String DRAFT_STATE = "Draft";
    private static final String SUBMITTED_STATE = "Submitted";
    private static final String ANONYMOUS = "anonymous";
    private static final String IGNORE_EVENT_POST = "ignoreEventPost";
    /**
     * The property name for allowing all members when allowPrivileged is enabled.
     */
    private static final String PN_ALLOW_ALL_MEMBERS = "allowAllMembers";

    //Subscription Types
    private static final String NOTIFICATION = "notification";
    private static final String SUBSCRIPTION = "subscription";
    private static final String ACTIVITYSTREAMS = "following";

    /**
     * The default property value for allowing all members when allowPrivileged is enabled.
     */
    private static final boolean PV_DEFAULT_ALLOW_ALL_MEMBERS = false;
    /**
     * List of properties that the comment will overwrite.
     */
    public static final String[] RESERVED_PROPERTY_NAMES = {CollabUser.PROP_EMAIL, CollabUser.PROP_NAME,
        CollabUser.PROP_WEBSITE};

    /**
     * The name of the property that holds a black list of attachment mime types that are unsafe.
     */
    public static final String PROPERTY_ATTACHMENT_TYPE_BLACKLIST = "attachmentTypeBlacklist";

    /**
     * The name of the property that holds the comment message.
     */
    public static final String PROP_MESSAGE = "message";
    private static final String PN_COMPOSED_FOR = "composedFor";

    private static final String PARAM_IS_MESSAGE_ENCODED = "messageEncoded";

    /**
     * Logger.
     */
    private static final Logger LOG = LoggerFactory.getLogger(AbstractCommentOperationService.class);

    private static final String PROP_ATT_TO_REMOVE = "attToRemove";
    public static final String PROP_CONTEXT_PATH = "contextPath";

    @Reference
    public SocialComponentFactoryManager componentFactoryManager;

    @Reference
    protected ScheduledPostService futurePostScheduler;

    /**
     * Resource Resolver used for the request.
     */
    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY, policy = ReferencePolicy.STATIC)
    protected ResourceResolverFactory resourceResolverFactory;

    /**
     * Resource Resolver used for the request.
     */
    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY, policy = ReferencePolicy.STATIC)
    protected Externalizer externalizer;

    /**
     * Sling settings service.
     */
    @Reference
    protected SlingSettingsService settingsService;

    @Reference
    protected EventAdmin eventAdmin;

    /**
     * UGC Limiter Service for checking contribution limits .
     */
    @Reference
    protected UGCLimiterService ugcLimiterService;

    /**
     * Sling repository for finding / creating nodes.
     */
    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY, policy = ReferencePolicy.STATIC)
    protected SlingRepository repository;

    @Reference
    protected InheritedOperationExtensionManager extensionManager;

    /**
     * White list field.
     */
    protected String[] fieldWhitelist;

    /**
     * Attachment type blacklist.
     */
    protected String[] attachmentTypeBlacklist;

    @SuppressWarnings("unused")
    private ComponentContext context;

    @Override
    public InheritedOperationExtensionManager getInheritedOperationExtensionManager() {
        return extensionManager;
    }

    /**
     * Extract the default comment properties from the specified {@link SlingHttpServletRequest} into the specified.
     * {@link Map}
     * @param request SlingHttpServletRequest
     * @param props Map
     * @param session Session
     * @throws RepositoryException RepositoryException
     * @throws OperationException OperationException
     */
    protected void getDefaultProperties(final SlingHttpServletRequest request, final Map props,
        final Session session) throws RepositoryException, OperationException {
        final Resource resource = request.getResource();
        final String name = getAuthorizableId(request, session);
        final CommentSystem cs = getCommentSystem(resource, session);
        final com.adobe.cq.social.commons.Comment comment = getComment(resource, session);
     
        //IsCreatedByPrivlegedUser
        final Boolean isCreatedByPrivilegedUser = CommentUtil.isUserPrivileged(cs.getResource(), resource.getResourceResolver(), name);
        props.put(PROPERTY_ISCREATEDBYPRIVUSER, isCreatedByPrivilegedUser);
        
        props.put(PROP_CONTEXT_PATH, request.getContextPath());
        // email
        String email = request.getParameter(com.adobe.cq.social.ugcbase.CollabUser.PROP_EMAIL);
        if (email == null) {
            email = "";
        }
        props.put(com.adobe.cq.social.ugcbase.CollabUser.PROP_EMAIL, email);

        // website
        String website = request.getParameter(com.adobe.cq.social.ugcbase.CollabUser.PROP_WEBSITE);
        if (website == null) {
            website = "";
        } else if (!"".equals(website) && !website.matches("^.*\\:\\/\\/.*$")) {
            website = "http://" + website;
        }
        props.put(com.adobe.cq.social.ugcbase.CollabUser.PROP_WEBSITE, website);

        // ip
        props.put(com.adobe.cq.social.commons.Comment.PROP_IP_ADDRESS, getClientIpAddr(request));

        // userAgent
        final String userAgent = request.getHeader("User-Agent");
        if (StringUtils.isNotBlank(userAgent)) {
            props.put(com.adobe.cq.social.commons.Comment.PROP_USER_AGENT, request.getHeader("User-Agent"));
        }

        // authorizable id
        final String authId = getAuthorizableId(request, session);
        if (StringUtils.isNotBlank(authId)) {
            props.put("authorizableId", authId);
        }

        // referer
        getReferrerProperty(request, props);

        // isMessageEncoded and wouldnt be saved in UGC
        final String isMsgEncoded = request.getParameter(PARAM_IS_MESSAGE_ENCODED);
        if (StringUtils.isNotBlank(isMsgEncoded)) {
            props.put(PARAM_IS_MESSAGE_ENCODED, Boolean.parseBoolean(isMsgEncoded));
        }

        // the message
        final String value = request.getParameter(PROP_MESSAGE);
        if (StringUtils.isNotBlank(value)) {
            props.put(PROP_MESSAGE, value);
        }

        // isDraft
        final String isDraft = request.getParameter(Comment.PROP_IS_DRAFT);
        if (StringUtils.isNotBlank(isDraft)) {
            props.put(Comment.PROP_IS_DRAFT, Boolean.parseBoolean(isDraft));
            // initial state
            // TODO: should be done by state machine
            props.put(PROPERTY_STATE, Boolean.parseBoolean(isDraft) ? DRAFT_STATE : SUBMITTED_STATE);
        }

        // check publish later properties
        final String isPublishLater = request.getParameter(Comment.PROP_IS_SCHEDULED);
        if (StringUtils.isNotBlank(isPublishLater)) {
            // publishDate
            final String publishDateString = request.getParameter(Comment.PROP_PUBLISH_DATE);
            if (StringUtils.isNotBlank(publishDateString)) {
                try {
                    Calendar publishDate;
                    publishDate = DateUtil.parseISO8601(publishDateString, TimeZone.getTimeZone("Etc/UTC"));
                    if (publishDate != null) {
                        props.put(Comment.PROP_PUBLISH_DATE, publishDate);
                    } else {
                        throw new OperationException("Publish date and time format is incorrect",
                            HttpServletResponse.SC_BAD_REQUEST);
                    }
                } catch (final InvalidDateException e) {
                    LOG.error("Invalid date format for publishDate %s while trying to create/update comment",
                        publishDateString);
                    throw new OperationException("Publish date and time format is incorrect",
                        HttpServletResponse.SC_BAD_REQUEST);
                }
                props.put(Comment.PROP_IS_SCHEDULED, Boolean.parseBoolean(isPublishLater));
            } else {
                throw new OperationException("Publish date is empty", HttpServletResponse.SC_BAD_REQUEST);
            }
        }
        // Check for UGC_URL
        final String ugcUrl = request.getParameter(Comment.PROP_UGC_URL);
        if (StringUtils.isNotBlank(ugcUrl)) {
            props.put(Comment.PROP_UGC_URL, ugcUrl);
        }
    }

    private void getReferrerProperty(final SlingHttpServletRequest request, final Map props)
        throws RepositoryException {
        final String referrerUrl = getReferrer(request);
        if (StringUtils.isNotBlank(referrerUrl)) {
            props.put(SocialComponent.PROP_REFERER, referrerUrl);
            // Use the request header value if it exists, otherwise, use the request parameter.
            try {
                final URL url = new URL(referrerUrl);
                props.put("Referer", url.getFile());  // only store relative path with parameters
            } catch (final MalformedURLException e) {
                // Still throw an error if the url starts with the regex.
                if (referrerUrl.matches("^https?:")) {
                    LOG.error("Error parsing referer url", e);
                } else if (StringUtils.startsWith(referrerUrl, "/")) {
                    // If the url doesn't start with http then we can just try and pass back the value we see,
                    // which in the tests was an absolute path, e.g. /whatever/this/thingy/is.html
                    LOG.info("Referrer URL does not have a protocol, returning passed in value {}", referrerUrl);
                    props.put("Referer", referrerUrl);
                } else {
                    LOG.error("Error parsing referer url", e);
                }
            }
        }
    }

    private String getClientIpAddr(final SlingHttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }

    private void addProp(final Map props, final String name, final SlingHttpServletRequest request,
        final Session session) throws RepositoryException {
        final String value = request.getParameter(name);
        if (StringUtils.isNotBlank(value)) {
            props.put(name, value);
        }
    }

    /**
     * Parse all the comment properties in the specified {@link SlingHttpServletRequest} and stored them in the
     * specified {@link Map}.
     * @param request the client http request
     * @param map the map used to store the comment properties
     * @param session the user session
     * @throws RepositoryException if there is an error occurs
     */
    protected void getCustomProperties(final SlingHttpServletRequest request, final Map map,
        final Session session) throws RepositoryException {

        final RequestParameterMap params = request.getRequestParameterMap();

        for (final String key : params.keySet()) {
            if (map.containsKey(key)) {
                continue;
            }
            if (StringUtils.equals(key, CHARSET_PROPERTY)) {
                continue;
            }
            // Block all properties with a colon to help mitigate CQ5-24037.
            if (!ArrayUtils.contains(fieldWhitelist, key) || key.contains(":")) {
                LOG.debug("skipped custom form field [{}], not in white list.", key);
                continue;
            }

            if (!ArrayUtils.contains(RESERVED_PROPERTY_NAMES, key)) {

                final RequestParameter[] values = params.get(key);
                if (values.length > 0 && values[0].isFormField()) {

                    final Object value =
                        (values.length == 1) ? values[0].getString() : request.getParameterValues(key);
                    if (null != value) {
                        if (key.equals(com.adobe.cq.social.ugcbase.CollabUser.PROP_NAME)
                                && ((String) value).length() == 0) {
                            LOG.debug("skipped custom form field \"userIdentifier\", empty value is not allowed.");
                            continue;
                        }
                        map.put(key, value);
                    }

                } else {
                    LOG.debug("skipped custom form field [{}], empty or binary not allowed.", key);
                }

            } else {
                LOG.debug("skipped custom form field [{}], matches reserved field name.", key);
            }
        }

    }

    @SuppressWarnings("unchecked")
    protected List filterAttachments(final List attachments, final CommentSystem cs) {
        if (BooleanUtils.toBoolean(cs.allowsAttachment())) {
            final List attList =
                (List) (List) attachments;
            final Set whitelist =
                cs.getAllowedFileTypes() == null ? null : new HashSet(cs.getAllowedFileTypes());
            final Iterable filtered =
                AttachmentUtils.getAttachmentsFromDataSources(attList,
                    new FileUploadSizeLimit(cs.getAttachmentSizeLimit(), cs.getAttachmentSizeLimit()), whitelist,
                    attachmentTypeBlacklist);
            final List filteredAttList = IteratorUtils.toList(filtered.iterator());
            return new ArrayList(filteredAttList);
        }
        return Collections.emptyList();
    }

    protected List getAttachmentsFromRequest(final SlingHttpServletRequest request, final CommentSystem cs) {
        if (BooleanUtils.toBoolean(cs.allowsAttachment())) {
            final RequestParameter[] fileRequestParameters = request.getRequestParameters("file");
            return CollabUtil.getAttachmentsFromRequest(fileRequestParameters, cs.getAttachmentSizeLimit(),
                cs.getAllowedFileTypes(), attachmentTypeBlacklist);
        }
        return Collections.emptyList();
    }

    public void removeAttachment(final Resource attachment, final ResourceResolver serviceUserResolver) {
        if (attachment != null) {
            Resource target = attachment;
            final SocialUtils socialUtils = attachment.getResourceResolver().adaptTo(SocialUtils.class);
            if (!socialUtils.hasModeratePermissions(attachment)
                    || !socialUtils.hasModeratePermissions(attachment.getParent())) {
                target = serviceUserResolver.getResource(attachment.getPath());
            }
            final Resource commentResource = target.getParent();
            final Comment comment = commentResource.adaptTo(Comment.class);
            if (comment != null) {
                comment.removeAttachment(attachment.getName());
            }
        }
    }

    /**
     * Return if it is in author mode.
     * @return if it is in author mode.
     */
    protected boolean isAuthorMode() {
        return settingsService != null && settingsService.getRunModes().contains("author");
    }

    /**
     * Checks if the request might have come from a bot.
     * @param request request to check for bot
     * @return if the request originated from a bot
     */
    protected boolean isBot(final SlingHttpServletRequest request) {
        final String botCheck = request.getParameter(com.adobe.cq.social.commons.Comment.PARAM_BOTCHECK);
        return botCheck == null || !botCheck.equals(com.adobe.cq.social.commons.Comment.VALUE_BOTCHECK);
    }

    /**
     * Retrieves the {@link ResourceResolver} based on the session.
     * @param session The {@link Session} for which to get a ResourceResolver.
     * @return The resource resolver.
     * @throws LoginException LoginException
     */
    protected ResourceResolver getResourceResolver(final Session session) throws LoginException {
        final Map authInfo = new HashMap();
        authInfo.put(JcrResourceConstants.AUTHENTICATION_INFO_SESSION, session);
        return resourceResolverFactory.getResourceResolver(authInfo);
    }

    /**
     * Get the comment system for the given resource.
     * @param r The resource for which to retrieve the comment system.
     * @param session The {@link Session}.
     * @return The {@link CommentSystem}.
     */
    protected CommentSystem getCommentSystem(final Resource r, final Session session) {
        Resource res;
        try {
            res = getResourceResolver(session).resolve(r.getPath());
        } catch (final LoginException e) {
            LOG.error("Unable to fetch resource resolver for session", e);
            return null;
        }
        if (null != res) {
            return res.adaptTo(CommentSystem.class);
        } else {
            return null;
        }
    }

    /**
     * Get the comment for the given resource.
     * @param r The resource for which to retrieve the comment.
     * @param session The {@link Session}.
     * @return The {@link Comment}.
     */
    protected Comment getComment(final Resource r, final Session session) {
        Resource res;
        try {
            res = getResourceResolver(session).resolve(r.getPath());
        } catch (final LoginException e) {
            LOG.error("Unable to fetch resource resolver for session", e);
            return null;
        }
        if (null != res) {
            return res.adaptTo(Comment.class);
        } else {
            return null;
        }
    }

    /**
     * Get the use id from the specified {@link SlingHttpServletRequest}; the method also validates if the session
     * user id and the user id specified in the {@link SlingHttpServletRequest} is the same.
     * @param request SlingHttpServletRequest
     * @param session Session
     * @return the authorized id
     * @throws OperationException OperationException
     */
    protected String getAuthorizableId(final SlingHttpServletRequest request, final Session session)
        throws OperationException {
        String userIdentifier = request.getParameter(com.adobe.cq.social.ugcbase.CollabUser.PROP_NAME);
        if (StringUtils.isBlank(userIdentifier)) {
            userIdentifier = getUserIdFromRequest(request, com.adobe.cq.social.ugcbase.CollabUser.ANONYMOUS);
        }
        final String sessionUserId = getUserIdFromRequest(request, null);
        String id = "";
        if (StringUtils.isNotBlank(sessionUserId)) {
            final boolean anonymous = (com.adobe.cq.social.ugcbase.CollabUser.ANONYMOUS).equals(sessionUserId);
            final boolean authorMode = isAuthorMode();
            if (!anonymous && authorMode) {
                final boolean userExists = userExists(userIdentifier, getRequestSession(request));
                final boolean hasPermissions = hasPermissions(userIdentifier, getRequestSession(request), session);
                // use node.getSession() because that's an admin session
                if (userExists && hasPermissions) {
                    id = userIdentifier;
                    if (!userIdentifier.equals(sessionUserId)) {
                        LOG.warn(
                            "host {} posted a comment with different userIdentifier ({}) than sessionUserId ({})",
                            new String[]{request.getRemoteAddr(), userIdentifier, sessionUserId});
                    }
                } else {
                    LOG.warn("host {} posted a comment with an unknown userIdentifier ({})", request.getRemoteAddr(),
                        userIdentifier);
                }
            } else if (!anonymous && !authorMode) {
                final String userId = sessionUserId;
                if (userIdentifier != null && !sessionUserId.equals(userIdentifier)) {
                    final StringBuilder exception = new StringBuilder("host ");
                    exception.append(request.getRemoteAddr());
                    exception.append("posted a comment with suspect userIdentifier (");
                    exception.append(userIdentifier);
                    exception.append("), sessionUserId (");
                    exception.append(sessionUserId);
                    exception.append(")");
                    final String exceptionMessage = exception.toString();
                    if (LOG.isWarnEnabled()) {
                        LOG.warn(exceptionMessage);
                    }
                    throw new OperationException(exceptionMessage, HttpServletResponse.SC_NOT_FOUND);
                }
                id = userId;
            } else {
                id = ANONYMOUS;
            }
        }
        return id;
    }

    /**
     * @param request SlingHttpServletRequest
     * @param defaultValue String
     * @return String user id
     */
    protected String getUserIdFromRequest(final SlingHttpServletRequest request, final String defaultValue) {
        String userIdentifier;
        final UserProperties up = request.getResourceResolver().adaptTo(UserProperties.class);
        userIdentifier = (up == null) ? null : up.getAuthorizableID();
        if (userIdentifier == null) {
            userIdentifier = defaultValue;
        }
        return userIdentifier;
    }

    /**
     * Return session userProperties.
     * @param request The sling request
     * @return session userProperties
     */
    protected UserProperties getSessionUserProperties(final SlingHttpServletRequest request) {
        return request.getResourceResolver().adaptTo(UserProperties.class);
    }

    /**
     * Return request session.
     * @param request The sling request
     * @return request session.
     */
    protected Session getRequestSession(final SlingHttpServletRequest request) {
        return request.getResourceResolver().adaptTo(Session.class);
    }

    /**
     * Return if user exists.
     * @param userId The user id
     * @param session The {@link Session}.
     * @return if user exists
     */
    protected boolean userExists(final String userId, final Session session) {
        try {
            final ResourceResolver resourceResolver = getResourceResolver(session);
            final UserManager userManager = resourceResolver.adaptTo(UserManager.class);
            final Authorizable user = userManager.getAuthorizable(userId);
            if (user != null) {
                return true;
            }
        } catch (final RepositoryException e) {
            LOG.debug("Error checking for user existence", e);
        } catch (final LoginException e) {
            LOG.debug("Error checking for user existence", e);
        }
        return false;
    }

    protected boolean isUserPrivileged(final CommentSystem cs, final Session session, final String userId) {
        ResourceResolver resolver = null;
        try {
            resolver = getResourceResolver(session);
        } catch (final LoginException e) {
            LOG.debug("Could not check for user privileges", e);
        }
        
        Boolean isUserPrivileged = CommentUtil.isUserPrivileged(cs.getResource(), resolver, userId);
        if (!isUserPrivileged && CommentUtil.getProperty(cs.getResource(), PN_ALLOW_ALL_MEMBERS,
                PV_DEFAULT_ALLOW_ALL_MEMBERS)) {
            return true;
        }
        return isUserPrivileged; 
    }

    /**
     * Return if user has permission.
     * @param userIdentifier The user id
     * @param requestSession The {@link Session}.
     * @param adminSession The administrator {@link Session}.
     * @return if user has permission.
     */
    protected boolean hasPermissions(final String userIdentifier, final Session requestSession,
        final Session adminSession) {
        try {
            if (StringUtils.isNotBlank(userIdentifier)) {
                final UserProperties userProperties =
                    getResourceResolver(requestSession).adaptTo(UserProperties.class);
                if (requestSession != null) {
                    return requestSession.hasPermission(userProperties.getResource(".").getPath(),
                        Session.ACTION_READ);
                }

            }
            return false;
        } catch (final LoginException e) {
            return false;
        } catch (final RepositoryException e) {
            return false;
        }
    }

    /**
     * Activate this component. Open the session and register event listeners.
     * @param context The component context
     */
    @Activate
    protected void activate(final ComponentContext context) {
        this.context = context;
        fieldWhitelist = OsgiUtil.toStringArray(context.getProperties().get(PROPERTY_FIELD_WHITELIST));
        attachmentTypeBlacklist =
            OsgiUtil.toStringArray(context.getProperties().get(PROPERTY_ATTACHMENT_TYPE_BLACKLIST));

    }

    /**
     * Obtain the Sling repository.
     * @return The repository used by this service.
     */
    protected SlingRepository getRepository() {
        return repository;
    }

    /**
     * Retrieves a {@link Node} identified by the given path.
     * @param path The path.
     * @param session The {@link Session}.
     * @return The node.
     */
    protected Resource getResource(final String path, final Session session) {
        try {
            return getResourceResolver(session).getResource(path);
        } catch (final LoginException e) {
            LOG.error("Error getching the resource resolver from session", e);
            return null;
        }
    }

    protected void postEvent(final SocialEvent event) {
        final EventAdmin localEventAdminRef = eventAdmin;
        if (null != localEventAdminRef) {
            localEventAdminRef.postEvent(event);
        }
    }

    protected Resource create(final Resource targetCommentSystemResource, final CommentSystem cs,
        final String author, final Map props, final List attachments,
        final Session session) throws OperationException {
        final U createOperation = getCreateOperation();
        performBeforeActions(createOperation, session, targetCommentSystemResource, props);
        if (cs == null) {
            throw new OperationException("Failed to get comment system for target '"
                    + targetCommentSystemResource.getPath() + "' ", HttpServletResponse.SC_NOT_FOUND);
        }

        if (cs.isClosed()) {
            throw new OperationException("Reply attempted on closed comment system: " + cs.getPath(),
                HttpServletResponse.SC_METHOD_NOT_ALLOWED);
        }

        String message;
        try {
            message = getStringProperty(PROP_MESSAGE, props);
        } catch (final RepositoryException e) {
            throw new OperationException("Failed to get the new message value", e,
                HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        }
        if (message == null || "".equals(message)) {
            // These messages now flow all the way into the UX, so be careful referencing more technical names
            throw new OperationException("Comment value is empty", HttpServletResponse.SC_BAD_REQUEST);
        }
        // If this is a reply, and the parent comment is closed, throw an exception
        final Comment parent = targetCommentSystemResource.adaptTo(Comment.class);
        if (parent != null && parent.isClosed()) {
            throw new OperationException("Reply attempted on closed comment: "
                    + targetCommentSystemResource.getPath(), HttpServletResponse.SC_BAD_REQUEST);
        }

        if (parent != null && !mayReply(targetCommentSystemResource, cs)) {
            throw new OperationException("Reply is not allowed: " + targetCommentSystemResource.getPath(),
                HttpServletResponse.SC_FORBIDDEN);
        }

        final long messageCharacterLimit = cs.getMessageCharacterLimit();
        final String normalizedMessage = Normalizer.normalize(message, Normalizer.Form.NFC);

        if (normalizedMessage.codePointCount(0, normalizedMessage.length()) > messageCharacterLimit) {
            throw new OperationException("Parameter " + PROP_MESSAGE + " exceeded character limit",
                HttpServletResponse.SC_BAD_REQUEST);

        }

        // check if UGC exists
        final boolean rootPathExists = getResource(cs.getRootPath(), session) != null;

        if (props.containsKey(PROP_MESSAGE)) {
            props.put(com.adobe.cq.social.commons.Comment.PROP_MESSAGE, props.get(PROP_MESSAGE));
            props.remove(PROP_MESSAGE);
        }

        if (props.containsKey(TAGS_PROPERTY)) {
            props.put(CQ_TAGS_PROPERTY, props.get(TAGS_PROPERTY));
            props.remove(TAGS_PROPERTY);
        }

        if (props.containsKey(Comment.PROP_PUBLISH_DATE)) {
            if (props.get(Comment.PROP_PUBLISH_DATE) instanceof Calendar) {
                final Calendar d = Calendar.getInstance();
                d.setTime(((Calendar) props.get(Comment.PROP_PUBLISH_DATE)).getTime());
                props.put(Comment.PROP_PUBLISH_DATE, d);
            }
        }

        // check cq:tags is in the correct format of String[]
        if (props.containsKey(CQ_TAGS_PROPERTY)) {
            final Object v = props.get(CQ_TAGS_PROPERTY);
            if (!(v instanceof String[])) {
                if (v instanceof String) {
                    if (String.valueOf(v).isEmpty()) {
                        props.remove(CQ_TAGS_PROPERTY);
                    } else {
                        props.put(CQ_TAGS_PROPERTY, new String[]{(String) v}); // convert to string array
                    }
                } else {
                    throw new OperationException("Parameter " + CQ_TAGS_PROPERTY + " is not a String Array",
                        HttpServletResponse.SC_BAD_REQUEST);
                }
            }
        }

        // Filter out controlled scf: prefixed properties
        for (final String key : props.keySet()) {
            if (StringUtils.startsWith(key, "scf:")) {
                props.remove(key);
            }
        }

        // date
        if (!props.containsKey(com.adobe.cq.social.commons.Comment.PROP_DATE)
                || (!(props.get(com.adobe.cq.social.commons.Comment.PROP_DATE) instanceof Calendar) && !(props
                    .get(com.adobe.cq.social.commons.Comment.PROP_DATE) instanceof Date))) {
            final Calendar cal = Calendar.getInstance();
            props.put(com.adobe.cq.social.commons.Comment.PROP_DATE, cal);
        }

        message = encodeMessageIfNeeded(message, props, cs.isRteEnabled());
        try {
            // create comment
            final com.adobe.cq.social.commons.Comment comment =
                cs.addComment(message, author, attachments, "", getResourceType(targetCommentSystemResource), props);
            if (SocialResourceUtils.isSocialResource(comment.getResource())) {
                final ModifiableValueMap vm = comment.getResource().adaptTo(ModifiableValueMap.class);
                if (vm != null) {
                    final String entityUrl = getEntityUrl(comment.getResource());
                    if (!StringUtils.isEmpty(entityUrl)) {
                        vm.put(SocialUtils.PN_ENTITY, entityUrl);
                    }
                    // For post-moderated, it is approved, non SRP will set the property via workflow
                    if (!cs.isModerated()) {
                        vm.put(Comment.PROP_APPROVED, true);
                    }
                    // add event topic
                    vm.put(Comment.PROP_EVENT_TOPIC, getEventTopic());
                    // add publishDate for regular published post
                    if (vm.get(Comment.PROP_PUBLISH_DATE, Calendar.class) == null
                            && !vm.get(Comment.PROP_IS_DRAFT, false)) {
                        vm.put(Comment.PROP_PUBLISH_DATE, Calendar.getInstance());
                    }
                }
            }

            String userId = author;
            if (props.containsKey(PN_COMPOSED_FOR)) {
                userId = comment.getProperty(PN_COMPOSED_FOR, author);
            }
            boolean throwEvent = true;
            if (comment.getProperty(Comment.PROP_IS_DRAFT, false)) {
                throwEvent = false;
                if (comment.getProperty(Comment.PROP_PUBLISH_DATE, Calendar.class) != null) {
                    final Calendar publishDate = comment.getProperty(Comment.PROP_PUBLISH_DATE, Calendar.class);
                    // if the scheduled publish time is in past or now, just continue as regular post
                    // and update the PN_DATE to the scheduled time;
                    // if the scheduled time is in future, let SearchUnscheduledPost handles it.
                    if (publishDate.compareTo(Calendar.getInstance()) <= 0) {
                        final ModifiableValueMap vm = comment.getResource().adaptTo(ModifiableValueMap.class);
                        vm.put(Comment.PROP_IS_DRAFT, false);
                        vm.put(NameConstants.PN_PAGE_LAST_MOD, publishDate);
                        vm.put(SocialUtils.PN_DATE, publishDate.getTime());
                        throwEvent = true;
                    }
                }
            } else if (cs.isModerated()) {
                throwEvent = false;
            }
            cs.save();
            LOG.info("Comment created: " + comment.getPath());

            final S commentComp = getSocialComponentForResource(comment.getResource());
            performAfterActions(createOperation, session, commentComp, props);
            if (throwEvent) {
                postCreateEvent(commentComp, userId);
            }
            return comment.getResource();
        } catch (final CommentException e) {
            cleanupFailure(session);
            throw new OperationException("Failed to create comment.", e, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        }

    }

    private String encodeMessageIfNeeded(final String msg, final Map props, final boolean isRTEEnabled) {
        String message = msg;
        // Msg is encoded if RTE if off
        // Msg is encoded if messageEncoded param is explicitly set and set to false
        boolean msgNeedsEncoding = !isRTEEnabled;
        if (props.containsKey(PARAM_IS_MESSAGE_ENCODED)) {
            final boolean isMsgEncoded = (Boolean) props.get(PARAM_IS_MESSAGE_ENCODED);
            if (isMsgEncoded) {
                msgNeedsEncoding = false;
            } else {
                msgNeedsEncoding = true;
            }
            props.remove(PARAM_IS_MESSAGE_ENCODED);
        }

        if (msgNeedsEncoding) {
            message = StringEscapeUtils.escapeHtml4(message);
        }
        return message;
    }

    /**
     * @param session
     * @throws OperationException
     */
    private void cleanupFailure(final Session session) {
        try {
            session.refresh(false);
        } catch (final RepositoryException e) {
            LOG.info("Failed to refresh the session", e);
        }

    }

    public Resource create(final Resource root, final String author, final Map props,
        final List attachments, final Session session) throws OperationException {
        final CommentSystem cs = getCommentSystem(root, session);
        final List filteredAttachments = filterAttachments(attachments, cs);
        return create(root, cs, author, props, filteredAttachments, session);
    }

    public Resource create(final SlingHttpServletRequest request, final Session session) throws OperationException {
        final Resource resource = request.getResource();

        final CommentSystem cs = getCommentSystem(resource, session);

        validateParameters(request);

        final String name = getAuthorizableId(request, session);
        if (name.equals(ANONYMOUS)) {
            throw new OperationException("Session is invalid",
                    HttpServletResponse.SC_UNAUTHORIZED);
        }

        if (ugcLimiterService == null) {
            // this throws IllegalStateException if reference can't be found for the service
            ugcLimiterService = Activator.getService(UGCLimiterService.class);
        }

        if (!ugcLimiterService.mayCreateUGC(request.getResourceResolver(), name)) {
            // Dont change the exception message. This exception is used in UI and it may stop working
            // Refer to CQ-97142
            throw new OperationException("Exceeded contribution limit of " + ugcLimiterService.getUgcLimit()
                    + " pieces of content per " + getDurationString(ugcLimiterService.getUgcDuration()) + ". " + getUGCLimitModerators(ugcLimiterService.getUgcLimitedToList()),
                HttpServletResponse.SC_FORBIDDEN);
        }

        if (!mayPost(request, cs, name)) {
            throw new OperationException("User not allowed to post to forum at " + cs.getPath(),
                HttpServletResponse.SC_PRECONDITION_FAILED);
        }

        final Map props = new HashMap();
        try {
            getDefaultProperties(request, props, session);
            /*
             * if (cs.getProperty(com.adobe.cq.social.commons.comments.api.Comment.PROP_USE_HREF_URL, Boolean.FALSE))
             * { if (props.get("Referer") == null && request.getHeader("Referer") != null) { final ResourceResolver
             * resolver = resource.getResourceResolver(); final Externalizer externalizer =
             * resolver.adaptTo(Externalizer.class); try { URL url = new URL(request.getHeader("Referer")); // this
             * part is a bit tricky, do we want to externalize this link? final String referer =
             * externalizer.relativeLink(request, url.getPath()); props.put("Referer", referer); } catch
             * (MalformedURLException e) { // This should never happen LOG.error("Error parsing referer url", e); } }
             * }
             */
            getCustomProperties(request, props, session);
        } catch (final CommentException e) {
            throw new OperationException("Failed to create comment", e,
                HttpServletResponse.SC_NON_AUTHORITATIVE_INFORMATION);
        } catch (final RepositoryException e) {
            throw new OperationException("Failed to get comment properties", e,
                HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        }
        
        String isComposedForUserExist;
        boolean isUserPrivileged = CommentUtil.isUserPrivileged(cs.getResource(), request.getResourceResolver(), name);
        if (!isUserPrivileged){
            try {
                 isComposedForUserExist = getStringProperty("composedFor", props);
                 if (isComposedForUserExist != null){
                    throw new OperationException("Failed to create comment on behalf of other user",
                        HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
               }
            } catch (RepositoryException e) {
                throw new OperationException("Failed to get the new composedFor property value", e,
                     HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            }
        }
        
        // create comment
        final List attachments = getAttachmentsFromRequest(request, cs);
        return create(resource, cs, name, props, attachments, session);

    }

    public Resource uploadImage(final SlingHttpServletRequest request, final Session session)
        throws OperationException {
        final Resource resource = request.getResource();
        final String name = getAuthorizableId(request, session);
        if (name.equals(ANONYMOUS)) {
            throw new OperationException("Session is invalid",
                    HttpServletResponse.SC_UNAUTHORIZED);
        }

        final CommentSystem cs = getCommentSystem(resource, session);

        if (!mayPost(request, cs, name)) {
            throw new OperationException("User not allowed to post to comment at " + cs.getPath(),
                HttpServletResponse.SC_PRECONDITION_FAILED);
        }
        final List attachments = getAttachmentsFromRequest(request, cs);
        final Map props = new HashMap();
        try {
            getDefaultProperties(request, props, session);
            getCustomProperties(request, props, session);
        } catch (final CommentException e) {
            LOG.error("Failed to get request properties at " + resource.getPath(), e);
            throw new OperationException("Failed to get request properties", e,
                HttpServletResponse.SC_NON_AUTHORITATIVE_INFORMATION);
        } catch (final RepositoryException e) {
            LOG.error("Failed to get request properties @ " + resource.getPath(), e);
            throw new OperationException("Failed to get comment properties", e,
                HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        }

        return uploadImageToTemp(resource, cs, name, attachments, session);

    }

    public Resource uploadImageToTemp(final Resource targetCommentSystemResource, final CommentSystem cs,
        final String author, final List attachments, final Session session) throws OperationException {

        if (cs == null) {
            throw new OperationException("Failed to get comment system for target '"
                    + targetCommentSystemResource.getPath() + "' ", HttpServletResponse.SC_NOT_FOUND);
        }
        final List filteredAttachments = filterAttachments(attachments, cs);

        // If this is a reply, and the parent comment is closed, throw an exception
        final Comment parent = targetCommentSystemResource.adaptTo(Comment.class);
        if (parent != null && parent.isClosed()) {
            throw new OperationException("Reply attempted on closed comment: "
                    + targetCommentSystemResource.getPath(), HttpServletResponse.SC_BAD_REQUEST);
        }

        if (attachments.size() == 1) { // only support 1 attachment for now
            final DataSource data = attachments.get(0);
            try {
                final Node uploadRoot =
                    UploadOperationUtils.createOrGetUploadResource(session, targetCommentSystemResource.getPath());
                if (attachments.size() == 1) { // only support 1 attachment for now
                    final String name = Text.getName(data.getName()); // IE sends us the full path.
                    final Node attachment =
                        UploadOperationUtils.addAttachment(uploadRoot, name, data.getInputStream(),
                            data.getContentType());
                    session.save();
                    return targetCommentSystemResource.getResourceResolver().getResource(attachment.getPath());
                }
            } catch (final RepositoryException e) {
                throw new OperationException("Could not upload image", e,
                    HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            } catch (final IOException e) {
                throw new OperationException("Could not upload image", e,
                    HttpServletResponse.SC_INTERNAL_SERVER_ERROR);

            }
        } else {
            throw new OperationException("Invalid number of attachment", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        }
        return null;
    }

    public Resource uploadImage(final Resource targetCommentSystemResource, final CommentSystem cs,
        final String author, final Map props, final List attachments,
        final Session session) throws OperationException {

        if (cs == null) {
            LOG.error("Failed to get comment system for target " + targetCommentSystemResource.getPath());
            throw new OperationException("Failed to get comment system for target '"
                    + targetCommentSystemResource.getPath() + "' ", HttpServletResponse.SC_NOT_FOUND);
        }
        final U uploadImageOperation = getUploadImageOperation();
        if (uploadImageOperation != null) {
            final Comment parent = targetCommentSystemResource.adaptTo(Comment.class);
            performBeforeActions(uploadImageOperation, session, parent.getResource(), props);

            final List filteredAttachments = filterAttachments(attachments, cs);

            // If this is a reply, and the parent comment is closed, throw an exception
            if (parent != null && parent.isClosed()) {
                LOG.error("Reply attempted on closed comment: " + targetCommentSystemResource.getPath());
                throw new OperationException("Reply attempted on closed comment: "
                        + targetCommentSystemResource.getPath(), HttpServletResponse.SC_BAD_REQUEST);
            }

            if (parent != null && !mayReply(targetCommentSystemResource, cs)) {
                LOG.error("Reply is not allowed: " + targetCommentSystemResource.getPath());
                throw new OperationException("Reply is not allowed: " + targetCommentSystemResource.getPath(),
                    HttpServletResponse.SC_FORBIDDEN);
            }
            if (attachments.size() == 1) { // only support 1 attachment for now
                final DataSource data = attachments.get(0);
                try {
                    final Resource image =
                        parent.addImage(data.getName(), data.getInputStream(), data.getContentType());
                    performAfterActions(uploadImageOperation, session,
                        getSocialComponentForResource(targetCommentSystemResource), props);
                    image.getResourceResolver().commit();
                    return image;
                } catch (final IOException e) {
                    LOG.error("IOException while trying to add image at " + targetCommentSystemResource.getPath());
                    throw new OperationException("IOException while trying to add image", e,
                        HttpServletResponse.SC_INTERNAL_SERVER_ERROR);

                }
            } else {
                LOG.error("Only one attachment supported " + targetCommentSystemResource.getPath());
                throw new OperationException("Invalid number of attachment",
                    HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            }
        } else {
            throw new OperationException("Unsupported operation", HttpServletResponse.SC_BAD_REQUEST);
        }
    }

    private String getStringProperty(final String key, final Map props) throws RepositoryException {
        final Object obj = props.get(key);
        if (obj == null) {
            return null;
        }

        if (obj instanceof Value) {
            return ((Value) obj).getString();
        } else {
            return obj.toString();
        }
    }

    protected void validateParameters(final SlingHttpServletRequest request) throws OperationException {
        final String message = request.getParameter(PROP_MESSAGE);
        if (message == null || "".equals(message)) {
            throw new OperationException("Comment value is empty", HttpServletResponse.SC_BAD_REQUEST);
        }
        if (isBot(request)) {
            throw new OperationException("Bot check failed: Parameter "
                    + com.adobe.cq.social.commons.Comment.PARAM_BOTCHECK + " is missing or has unexpected value",
                HttpServletResponse.SC_PRECONDITION_FAILED);
        }
    }

    /**
     * Indicates whether the user within the given resource resolver has enough permissions to post to the given
     * forum. If no posts to this comment system have yet been made, the permissions on the
     * {@link com.adobe.cq.social.commons.CommentSystem#PATH_UGC} path are checked instead.
     * @param request The {@link org.apache.sling.api.SlingHttpServletRequest} for which permissions are being
     *            validated.
     * @param cs The {@link com.adobe.cq.social.commons.CommentSystem} the comment system to check permissions for.
     * @param userId The user for who is trying post.
     * @return true if the user may post.
     */
    protected boolean mayPost(final SlingHttpServletRequest request, final CommentSystem cs, final String userId) {
        final ResourceResolver resolver = request.getResourceResolver();
        if (null != resolver && null != cs) {

            if (userId.equals(ANONYMOUS)) {
                return false;
            }
            final ResourceResolver ugcWriterResovler = cs.getResource().getResourceResolver();
            Resource r = ugcWriterResovler.getResource(cs.getPath());
            final SocialUtils socialUtils = ugcWriterResovler.adaptTo(SocialUtils.class);
            if (null != r && null != socialUtils) {
                r = ugcWriterResovler.getResource(socialUtils.resourceToACLPath(r));
            }
            if (null == r) {
                // There is a comment system in the content but nothing is posted yet so check the page the forum
                // resides
                // on to determine if a user can post.
                // resourceToUGCPath adds back a lot of extra stuff like jcr:content we can't have so we need to
                // construct the path by hand.
                r = resolver.getResource(CommentSystem.PATH_UGC);
            }
            if (r != null) {
                return CollabUtil.canAddNode(resolver.adaptTo(Session.class), r.getPath());
            }
        }
        return false;
    }

    protected boolean mayEdit(final SlingHttpServletRequest request, final CommentSystem cs, final String userId)
        throws OperationException {
        final boolean mayEdit =
            (CollabUtil.hasModeratePermissions(request.getResource()) || CollabUtil.isResourceOwner(request
                .getResource()));
        if (mayEdit) {
            return true;
        }
        final ValueMap props = request.getResource().adaptTo(ValueMap.class);
        final String composedBy = props.get(Comment.PROP_COMPOSED_BY, "");
        return mayEdit || StringUtils.equals(userId, composedBy);
    }

    /**
     * Check if the specified root is configured for replying to a comment
     * @param root the comment root
     * @param cs the CommentSystem
     * @return boolean
     * @throws OperationException OperationException
     */
    protected boolean mayReply(final Resource root, final CommentSystem cs) throws OperationException {
        return cs.allowsReplies();
    }

    protected boolean mayDelete(final SlingHttpServletRequest request, final CommentSystem cs, final String userId)
        throws OperationException {
        return this.mayEdit(request, cs, userId);
    }

    protected String getAuthorFromRequest(final SlingHttpServletRequest request) {
        String name = request.getParameter(com.adobe.cq.social.ugcbase.CollabUser.PROP_NAME);
        if (StringUtils.isBlank(name)) {
            final UserProperties up = request.getResourceResolver().adaptTo(UserProperties.class);
            name = (up == null) ? null : up.getAuthorizableID();
            if (name == null) {
                name = com.adobe.cq.social.ugcbase.CollabUser.ANONYMOUS;
            }
        }
        return name;
    }

    /**
     * {@inheritDoc}
     */
    public Resource update(final SlingHttpServletRequest request, final Session session) throws OperationException {
        final Resource resource = request.getResource();

        final CommentSystem cs = getCommentSystem(resource, session);

        validateParameters(request);

        final String userId = getAuthorizableId(request, session);
        if (userId.equals(ANONYMOUS)) {
            throw new OperationException("Session is invalid",
                    HttpServletResponse.SC_UNAUTHORIZED);
        }
        if (!mayEdit(request, cs, userId)) {
            throw new OperationException("User not allowed to edit UGC at " + cs.getPath(),
                HttpServletResponse.SC_PRECONDITION_FAILED);
        }
        
        final Map props = new HashMap();
        try {
            getDefaultProperties(request, props, session);
            getCustomProperties(request, props, session);
        } catch (final CommentException e) {
            throw new OperationException("Failed to update UGC", e,
                HttpServletResponse.SC_NON_AUTHORITATIVE_INFORMATION);
        } catch (final RepositoryException e) {
            throw new OperationException("Failed to get comment properties", e,
                HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        }

        String isComposedForUserExist;
        boolean isUserPrivileged = CommentUtil.isUserPrivileged(cs.getResource(), request.getResourceResolver(), userId);
        if (!isUserPrivileged){
            try {
                isComposedForUserExist = getStringProperty("composedFor", props);
                if (isComposedForUserExist != null){
                    throw new OperationException("Failed to create comment on behalf of other user",
                        HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
               }
            } catch (RepositoryException e) {
                throw new OperationException("Failed to get the new composedFor property value", e,
                   HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            }
        }
        
        final List attachments = getAttachmentsFromRequest(request, cs);
        RequestParameter[] requestParameters = request.getRequestParameters("attachmentsTobeDeleted");
        if (ArrayUtils.isNotEmpty(requestParameters)) {
            for (RequestParameter requestParameter : requestParameters) {
                String attachmentPath = requestParameter.getString();
                removeAttachment(request, session, attachmentPath);
            }
        }
        return update(resource, cs, props, attachments, session, userId);
    }

    public void removeAttachment(final SlingHttpServletRequest request, final Session session)
        throws OperationException {
        final String attachmentPath = request.getParameter(PROP_ATT_TO_REMOVE);
        removeAttachment(request, session, attachmentPath);
    }

    private void removeAttachment(final SlingHttpServletRequest request, final Session session,
            final String attachmentPath) throws OperationException {
        if (StringUtils.isEmpty(attachmentPath)) {
            throw new OperationException("Attachment path is empty", HttpServletResponse.SC_BAD_REQUEST);
        }
        final Resource resource = request.getResource();
        final String userId = getAuthorizableId(request, session);
        if (userId.equals(ANONYMOUS)) {
            throw new OperationException("Session is invalid",
                    HttpServletResponse.SC_UNAUTHORIZED);
        }
        final CommentSystem cs = getCommentSystem(resource, session);
        if (!mayEdit(request, cs, userId)) {
            throw new OperationException("User not allowed to edit UGC at " + cs.getPath(),
                HttpServletResponse.SC_PRECONDITION_FAILED);
        }
        removeAttachment(resource, attachmentPath, session);
    }

    public void removeAttachment(final Resource commentResource, final String attachment, final Session session)
        throws OperationException {
        final Comment comment = getComment(commentResource, session);
        if (comment == null) {
            throw new OperationException("Unable to remove attachment - " + attachment + "for comment - "
                    + comment.getPath(), HttpServletResponse.SC_BAD_REQUEST);
        }
        if (comment.getAttachmentMap() == null || !comment.getAttachmentMap().containsKey(attachment)) {
            throw new OperationException("Unable to remove attachment - " + attachment + "for comment - "
                    + comment.getPath(), HttpServletResponse.SC_BAD_REQUEST);
        }
        try {
            comment.removeAttachment(attachment);
            comment.getResource().getResourceResolver().commit();
        } catch (final CommentException e) {
            throw new OperationException("Unable to remove attachment for comment " + comment.getPath(), e,
                HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        } catch (final PersistenceException e) {
            throw new OperationException("Unable to remove attachment for comment " + comment.getPath(), e,
                HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        }
    }

    protected Resource update(final Resource commentResource, final CommentSystem cs,
        final Map props, final List attachments, final Session session,
        final String author) throws OperationException {
        final com.adobe.cq.social.commons.Comment comment = getComment(commentResource, session);
        if (comment == null) {
            throw new OperationException("Failed to get Commment for target " + commentResource.getPath(),
                HttpServletResponse.SC_NOT_FOUND);
        }
        if (comment.isClosed() && !CollabUtil.hasModeratePermissions(commentResource)) {
            throw new OperationException("Update attempted on closed comment: " + commentResource.getPath(),
                HttpServletResponse.SC_BAD_REQUEST);
        }
        final U updateOperation = getUpdateOperation();
        performBeforeActions(updateOperation, session, comment.getResource(), props);
        if (cs == null) {
            throw new OperationException("Failed to get comment system for target '"
                    + comment.getResource().getPath() + "' ", HttpServletResponse.SC_NOT_FOUND);
        }
        try {
            // TODO move this to Comment and don't use Node API
            final ModifiableValueMap properties = comment.getResource().adaptTo(ModifiableValueMap.class);
            // original publish date if any
            final Calendar oldPublishDate = properties.get(Comment.PROP_PUBLISH_DATE, Calendar.class);
            final boolean usedToBeDraft = properties.get(Comment.PROP_IS_DRAFT, false);
            // Rename the tags property to cq:tags
            if (props.containsKey(TAGS_PROPERTY)) {
                props.put(CQ_TAGS_PROPERTY, props.get(TAGS_PROPERTY));
                props.remove(TAGS_PROPERTY);
            }
            // remove any existent tags to allow property overwrite
            properties.remove(CQ_TAGS_PROPERTY);
            if (props.containsKey(PROP_MESSAGE)) {
                final long messageCharacterLimit = cs.getMessageCharacterLimit();
                String message = CollabUtil.getValueString(props.get(PROP_MESSAGE));
                if (message == null) {
                    throw new OperationException("Null value for comment message.",
                        HttpServletResponse.SC_BAD_REQUEST);
                }
                final String normalizedMessage = Normalizer.normalize(message, Normalizer.Form.NFC);

                if (normalizedMessage.codePointCount(0, normalizedMessage.length()) > messageCharacterLimit) {
                    throw new OperationException("Parameter " + PROP_MESSAGE + " exceeded character limit",
                        HttpServletResponse.SC_BAD_REQUEST);

                }
                message = encodeMessageIfNeeded(message, props, cs.isRteEnabled());
                properties.put(com.adobe.cq.social.commons.Comment.PROP_MESSAGE, message);
            }
            for (final Entry entry : props.entrySet()) {
                if (!StringUtils.equals(entry.getKey(), PROP_MESSAGE)) {
                    properties.put(entry.getKey(), entry.getValue());
                }
            }
            // trigger moderation if it is pre-moderated
            if (cs.isModerated()) {
                properties.remove(com.adobe.cq.social.commons.Comment.PROP_APPROVED);
            }
            properties.put(com.adobe.cq.social.commons.Comment.PROP_MODERATE, Boolean.TRUE);
            properties.put(NameConstants.PN_PAGE_LAST_MOD, Calendar.getInstance());
            if (CollabUtil.hasModeratePermissions(comment.getResource())) {
                properties.put(NameConstants.PN_PAGE_LAST_MOD_BY, session.getUserID());
            } else {
                properties.put(NameConstants.PN_PAGE_LAST_MOD_BY, comment.getAuthor().getId());
            }
            updateAttachments(comment, attachments);

            /**
             * users can save comment as publish immediately, draft, and publish later at any time. this is expected
             * behavior when a comment is switched between the three modes. immediately -> immediately [throw Update
             * event] immediately -> draft [--] immediately -> later [*] draft -> immediately [throw Create event]
             * draft -> draft [--] draft -> later [*] later -> immediately [throw Create event][**] later -> draft
             * [--][**] later -> later [*][***] [*] if scheduled publish time is passed, treat it as publish
             * immediately. [**] cancel the job has been scheduled for the comment, if any. [***] if the publish time
             * is changed, cancel old job.
             */
            boolean isCurrentDraft = false;
            if (props.containsKey(Comment.PROP_IS_DRAFT)) {
                isCurrentDraft = (Boolean) props.get(Comment.PROP_IS_DRAFT);
            }
            Calendar currentPublishDate = null;
            if (props.containsKey(Comment.PROP_PUBLISH_DATE)) {
                currentPublishDate = (Calendar) props.get(Comment.PROP_PUBLISH_DATE);
            }
            // cancel job when publish-later is changed
            boolean cancelJob = false;
            boolean forcePublish = false;
            if (!isCurrentDraft && usedToBeDraft && oldPublishDate != null) {
                // publish-later -> publish immediately
                cancelJob = true;
            } else if (isCurrentDraft && oldPublishDate != null
                    && (currentPublishDate == null || oldPublishDate.compareTo(currentPublishDate) != 0)) {
                // 1. publish-later date is updated
                // 2. publish-later -> draft
                cancelJob = true;
            }
            if (isCurrentDraft && currentPublishDate != null
                    && currentPublishDate.compareTo(Calendar.getInstance()) <= 0) {
                // if the scheduled publish time is in past or now, so publish it now
                properties.put(Comment.PROP_IS_DRAFT, Boolean.FALSE);
                properties.put(NameConstants.PN_PAGE_LAST_MOD, currentPublishDate);
                properties.put(SocialUtils.PN_DATE, currentPublishDate.getTime());
                cancelJob = true;
                forcePublish = true;
            }
            if (cancelJob) {
                // cancel job if scheduled
                final String oldJob = properties.get(Comment.PROP_PUBLISH_JOB_ID, String.class);
                if (currentPublishDate == null) {
                    properties.remove(Comment.PROP_PUBLISH_DATE);
                }
                if (StringUtils.isNotEmpty(oldJob)) {
                    futurePostScheduler.unschedule(oldJob);
                    properties.remove(Comment.PROP_PUBLISH_JOB_ID);
                }
            }
            if (!isCurrentDraft && usedToBeDraft) {
                // 1. draft -> publish immediately
                // 2. publish-later -> publish immediately
                properties.put(Comment.PROP_PUBLISH_DATE, Calendar.getInstance());
            }
            comment.getResource().getResourceResolver().commit();
            final com.adobe.cq.social.commons.Comment updatedComment =
                comment.getResource().adaptTo(com.adobe.cq.social.commons.Comment.class);
            final S commentComp = getSocialComponentForResource(updatedComment.getResource());

            if (!isCurrentDraft) {
                if (!usedToBeDraft) {
                    // regular post update
                    postUpdateEvent(commentComp, author);
                } else {
                    // 1. draft -> publish immediately
                    // 2. publish-later -> publish immediately
                    postCreateEvent(commentComp, author);
                }
            } else if (forcePublish) {
                postCreateEvent(commentComp, author);
            }

            performAfterActions(updateOperation, session, commentComp, props);
            return updatedComment.getResource();
        } catch (final RepositoryException e) {
            throw new OperationException("Failed to update comment", e, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        } catch (final PersistenceException e) {
            throw new OperationException("Failed to update comment", e, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        } catch (final IllegalArgumentException e) {
            throw new OperationException("Failed to update comment", e, HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        }
    }

    protected void updateAttachments(final Comment comment, final List attachments) {
        if (attachments != null) {
            comment.addAttachments(attachments);
        }
    }

    public Resource update(final Resource commentResource, final Map props,
        final List attachments, final Session session) throws OperationException {
        final CommentSystem cs = getCommentSystem(commentResource, session);
        return update(commentResource, cs, props, attachments, session, commentResource.getResourceResolver()
            .getUserID());
    }

    public void delete(final SlingHttpServletRequest request, final Session session) throws OperationException {
        delete(request, session, Collections. emptyMap());
    }
    /**
     * Delete a {@link Comment} specified by the {@link SlingHttpServletRequest} using the specified {@link Session}
     * @param request SlingHttpServletRequest
     * @param session Session
     * @param conditionals params deciding execution flow
     * @throws OperationException OperationException
     */
    public void delete(final SlingHttpServletRequest request, final Session session, final Map conditionals) throws OperationException {
        final Resource reqResource = request.getResource();
        final CommentSystem cs = getCommentSystem(reqResource, session);
        final com.adobe.cq.social.commons.Comment comment = getComment(reqResource, session);
        final boolean isDraft = comment.getProperty(Comment.PROP_IS_DRAFT, false);
        final String userId = getAuthorizableId(request, session);

        if (userId.equals(ANONYMOUS)) {
            throw new OperationException("Session is invalid",
                    HttpServletResponse.SC_UNAUTHORIZED);
        }

        // if draft - delete should be allowed even if delete operation is not enabled
        if (!cs.allowsDelete() && !isDraft) {
            throw new OperationException("Deletion is not enabling.", HttpServletResponse.SC_FORBIDDEN);
        }
        if (!mayDelete(request, cs, userId)) {
            throw new OperationException("User not allowed to delete " + cs.getPath(),
                HttpServletResponse.SC_NOT_FOUND);
        }

        if (conditionals.isEmpty()) {
            delete(reqResource, cs, session);
        } else {
            delete(reqResource, cs, session, conditionals);
        }


    }

    public void delete(final Resource commentResource, final Session session) throws OperationException {
        delete(commentResource, session, Collections. emptyMap());
    }

    public void delete(final Resource commentResource, final Session session, final Map conditionals) throws OperationException {
        final CommentSystem cs = getCommentSystem(commentResource, session);
        delete(commentResource, cs, session, conditionals);
    }

    public Resource move(final SlingHttpServletRequest request, final Session session) throws OperationException {
        final String resourcePath = request.getParameter("resourcePath");
        final String parentPath = request.getParameter("parentPath");
        final ResourceResolver resolver = request.getResourceResolver();
        if (null == resolver) {
            throw new OperationException("Request does not provide a resource resolver",
                HttpServletResponse.SC_FORBIDDEN);
        }
        final Resource resource = resolver.getResource(resourcePath);
        if (null == resource) {
            throw new OperationException("The resource to be moved could not be found at " + resourcePath,
                HttpServletResponse.SC_NOT_FOUND);
        }
        final Resource parent = resolver.getResource(parentPath);
        if (null == parent) {
            throw new OperationException("The target resource could not be found at " + parentPath,
                HttpServletResponse.SC_NOT_FOUND);
        }
        return move(resource, parent, request.getResourceResolver().adaptTo(Session.class));
    }

    public Resource move(final Resource commentResource, final Resource parentResource, final Session session)
        throws OperationException {
        final ResourceResolver resolver = commentResource.getResourceResolver();
        final String resourcePath = commentResource.getPath();
        final String parentPath = parentResource.getPath();
        final SocialUtils socialUtils = resolver.adaptTo(SocialUtils.class);
        final String testPath = resourcePath.substring(0, resourcePath.lastIndexOf("/"));
        final Boolean needMove =
            SocialResourceUtils.isCloudUGC(resourcePath) ? !socialUtils.resourceToUGCStoragePath(parentResource)
                .equals(testPath) : !parentPath.equals(testPath);

        if (needMove) {
            try {
                if (!socialUtils.hasModeratePermissions(commentResource)
                        || !socialUtils.hasModeratePermissions(parentResource)) {
                    throw new OperationException(
                        "The user does not have sufficient privileges to perform the move operation",
                        HttpServletResponse.SC_FORBIDDEN);
                }
                if (!SocialResourceUtils.isCloudUGC(parentPath)) {
                    session.checkPermission(socialUtils.resourceToUGCStoragePath(parentResource),
                        Session.ACTION_ADD_NODE);
                } else {
                    session.checkPermission(parentPath, Session.ACTION_ADD_NODE);
                }
            } catch (final AccessControlException e) {
                throw new OperationException(
                    "The user does not have sufficient privileges to perform the move operation", e,
                    HttpServletResponse.SC_FORBIDDEN);
            } catch (final RepositoryException e) {
                throw new OperationException("Unable to complete a permission check", e,
                    HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            }

            final SocialResourceProvider srp = socialUtils.getConfiguredProvider(commentResource);
            try {
                final Resource newResource = srp.move(resolver, resourcePath, parentPath);
                if (null == newResource) {
                    srp.revert(resolver);
                    throw new OperationException("Unable to move " + resourcePath + " to " + parentPath,
                        HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
                }
                // perform before actions for creating the new resource
                final U createOperation = getCreateOperation();

                final Map props = newResource.adaptTo(ModifiableValueMap.class);

                final S commentComp = getSocialComponentForResource(newResource);

                // Check if pinning allowed
                final Boolean allowPinning = commentComp.getConfiguration().isPinAllowed();
                final Boolean isPinned =
                    (Boolean) (props.get(com.adobe.cq.social.commons.Comment.PROP_PINNED) == null ? false : props
                        .get(com.adobe.cq.social.commons.Comment.PROP_PINNED));

                if (!allowPinning && isPinned) {
                    props.remove(com.adobe.cq.social.commons.Comment.PROP_PINNED);
                }

                performBeforeActions(createOperation, session,
                    resolver.getResource((String) props.get(SocialUtils.PN_CS_ROOT)), props);

                srp.commit(resolver);

                // perform after actions for create
                performAfterActions(createOperation, session, commentComp, props);
                postCreateEvent(commentComp, session.getUserID());

                return newResource;
            } catch (final PersistenceException e) {
                srp.revert(resolver);
                throw new OperationException("Unable to move " + resourcePath + " to " + parentPath, e,
                    HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            }
        } else {
            return commentResource;
        }
    }

    public Resource changeState(final Resource commentResource, final String author, final Map props,
        final List attachments, final Session session) throws OperationException {
        final CommentSystem cs = getCommentSystem(commentResource, session);
        return changeState(commentResource, cs, author, props, session);
    }

    public Resource changeState(final SlingHttpServletRequest request, final Session session)
        throws OperationException {
        final Resource resource = request.getResource();
        final String name = getAuthorizableId(request, session);
        final CommentSystem cs = getCommentSystem(resource, session);
        final com.adobe.cq.social.commons.Comment comment = getComment(resource, session);

        // get properties
        final Map props = new HashMap();
        try {
            getDefaultProperties(request, props, session);
            // the state operation
            final String operation = request.getParameter(PROPERTY_STATE_OPERATION);
            if (StringUtils.isNotBlank(operation)) {
                props.put(PROPERTY_STATE_OPERATION, operation);
            }

            // toState
            final String toState = request.getParameter(PROPERTY_TOSTATE);
            if (StringUtils.isNotBlank(toState)) {
                props.put(PROPERTY_TOSTATE, toState);
            }

            // is addReply
            final String isAddReply = request.getParameter(PROPERTY_ADD_REPLY);
            if (StringUtils.isNotBlank(isAddReply)) {
                props.put(PROPERTY_ADD_REPLY, Boolean.parseBoolean(isAddReply));
            }

            getCustomProperties(request, props, session);
        } catch (final CommentException e) {
            throw new OperationException("Failed to change comment state", e,
                HttpServletResponse.SC_NON_AUTHORITATIVE_INFORMATION);
        } catch (final RepositoryException e) {
            throw new OperationException("Failed to get comment properties", e,
                HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        }
        return changeState(resource, cs, name, props, session);
    }

    protected Resource changeState(final Resource commentResource, final CommentSystem cs, final String author,
        final Map props, final Session session) throws OperationException {
        final com.adobe.cq.social.commons.Comment comment = getComment(commentResource, session);
        if (comment == null) {
            throw new OperationException("Failed to get Commment for target " + commentResource.getPath(),
                HttpServletResponse.SC_NOT_FOUND);
        }
        if (comment.isClosed() && !CollabUtil.hasModeratePermissions(commentResource)) {
            throw new OperationException("Update attempted on closed comment: " + commentResource.getPath(),
                HttpServletResponse.SC_BAD_REQUEST);
        }
        final U changeStateOperation = getChangeStateOperation();
        performBeforeActions(changeStateOperation, session, comment.getResource(), props);
        if (cs == null) {
            throw new OperationException("Failed to get comment system for target '"
                    + comment.getResource().getPath() + "' ", HttpServletResponse.SC_NOT_FOUND);
        }

        if (!props.containsKey(PROPERTY_STATE_OPERATION)) {
            throw new OperationException("Cannot find state operation from request",
                HttpServletResponse.SC_BAD_REQUEST);
        }
        final String operation = (String) props.get(PROPERTY_STATE_OPERATION);
        if (!props.containsKey(PROPERTY_TOSTATE)) {
            throw new OperationException("Cannot find tostate from request", HttpServletResponse.SC_BAD_REQUEST);
        }
        final String toState = (String) props.get(PROPERTY_TOSTATE);

        // TODO: validate operation - should be in state machine
        final StateMachine stateMachine = commentResource.adaptTo(StateMachine.class);
        final S commentComp = getSocialComponentForResource(comment.getResource());
        final State state = stateMachine.getState(commentComp.getState());
        String role = null;
        boolean validOp = false;
        if (state != null) {
            if (CollabUtil.hasModeratePermissions(commentResource)) {
                validOp = state.canPerformOperation(MODERATORS_ROLE, operation);
                role = MODERATORS_ROLE;
            }
            if (!validOp && CollabUtil.isResourceOwner(commentResource)) {
                validOp = state.canPerformOperation(OWNER_ROLE, operation);
                role = OWNER_ROLE;
            }
        } else {
            throw new OperationException("Failed to get state for target " + commentResource.getPath(),
                HttpServletResponse.SC_NOT_FOUND);
        }
        if (!validOp) {
            throw new OperationException("Failed to change comment state",
                HttpServletResponse.SC_NON_AUTHORITATIVE_INFORMATION);
        }

        // change state of the comment
        state
            .performOperation(commentResource, comment.getResource().getResourceResolver(), role, operation, toState);

        // if a message is included, create a reply
        // this only works for review idea operation
        // save operation registration for later
        Resource replyRes = null;
        if (props.containsKey(PROPERTY_ADD_REPLY) && ((Boolean) props.get(PROPERTY_ADD_REPLY)).booleanValue()) {
            replyRes = create(commentResource, cs, author, props, Collections.emptyList(), session);
        }

        postChangeStateEvent(commentComp, author);

        performAfterActions(changeStateOperation, session, commentComp, props);
        return replyRes == null ? commentResource : replyRes;

    }

    public void delete(final Resource commentResource, final CommentSystem cs, final Session session)
            throws OperationException {
        delete(commentResource, cs, session, Collections. emptyMap());
    }

    /**
     * Process a comment delete request sent by a client. Sends back a 204 if the delete succeeded with no entity.
     * @param commentResource the comment to delete
     * @param cs the comment system
     * @param session the session used to delete
     * @param conditionals Map
     * @throws OperationException when the session cannot save the changes in the comment system
     */
    public void delete(final Resource commentResource, final CommentSystem cs, final Session session, final Map conditionals)
        throws OperationException {
        LOG.debug("delete for  {} ", commentResource.getPath());

        final com.adobe.cq.social.commons.Comment comment = getComment(commentResource, session);
        if (comment == null) {
            throw new OperationException("Unable to get comment for resource at " + commentResource.getPath(),
                HttpServletResponse.SC_NOT_FOUND);
        }
        if (comment.isClosed() && !CollabUtil.hasModeratePermissions(commentResource)) {
            throw new OperationException("Delete attempted on closed comment: " + commentResource.getPath(),
                HttpServletResponse.SC_BAD_REQUEST);
        }
        final S commentComp = getSocialComponentForResource(comment.getResource());
        final U deleteOperation = getDeleteOperation();
        performBeforeActions(deleteOperation, session, comment.getResource(), Collections.emptyMap());
        commentComp.getParentComponent(); // Force this to be read before the data goes away.
        final String author = commentResource.getResourceResolver().getUserID();

        // handle draft and scheduled posts
        boolean throwEvent = true;
        final ValueMap properties = comment.getResource().adaptTo(ModifiableValueMap.class);
        final Boolean isDraft = properties.get(Comment.PROP_IS_DRAFT, Boolean.class);
        if (isDraft != null && isDraft) {
            throwEvent = false;
            final String oldJob = properties.get(Comment.PROP_PUBLISH_JOB_ID, String.class);
            // cancel previous job
            if (StringUtils.isNotEmpty(oldJob)) {
                futurePostScheduler.unschedule(oldJob);
            }
        }

        final List comments = new ArrayList();
        CommentUtil.getComments(comment, comments);

        final ResourceResolver resolver = comment.getResource().getResourceResolver();
        comment.remove();
        comment.getCommentSystem().save();
        if (throwEvent && (conditionals.get(IGNORE_EVENT_POST) == null || (Boolean)conditionals.get(IGNORE_EVENT_POST).equals(false))) {
            postDeleteEvent(commentComp, author);
        }

        deleteActivities(comments, resolver);
        deleteRelationships(commentResource, resolver);
        performAfterActions(deleteOperation, session, commentComp, Collections.emptyMap());
    }

    private void deleteActivities(final List comments, final ResourceResolver resolver)
        throws OperationException {
        for (final String commentPath : comments) {
            final List activityResources = CommentUtil.getActivitiesForComment(resolver, commentPath);
            if (activityResources != null) {
                try {
                    for(Resource activityResource:activityResources) {
                        resolver.delete(activityResource);
                        resolver.commit();
                    }
                } catch (final PersistenceException e) {
                    LOG.warn("Failed to delete activity for comment '{}'", commentPath);
                }
            }
        }
    }

    private void deleteRelationships(final Resource commentResource, final ResourceResolver resolver) {
        String types[] = {SUBSCRIPTION, NOTIFICATION, ACTIVITYSTREAMS};
        final SocialGraph graph = resolver.adaptTo(SocialGraph.class);
        final Vertex subscriptionTargetNode = graph.getVertex(commentResource.getPath());
        if (subscriptionTargetNode != null ) {
            for (int i = 0; i < types.length; i++) {
                final Iterable relationships = subscriptionTargetNode.getRelationships(Direction.INCOMING, types[i]);
                if (relationships != null) {
                    for (Relationship relationship : relationships) {
                        relationship.delete();
                    }
                }
            }
        }
        try {
            resolver.commit();
        } catch (PersistenceException ex) {
            LOG.warn("Failed to delete subscriptions for comment '{}'", commentResource.getPath());
        }
    }

    /**
     * Collects the paths with the deepest nodes coming first so they can be deleted in a bottom up order.
     * @param resolver resolver to use to get resource from string path
     * @param path starting path
     * @return list of paths under the current node
     * @throws RepositoryException when the node cannot be found
     */
    private List getPaths(final ResourceResolver resolver, final String path) throws RepositoryException {
        // Collect all the child nodes of this node, otherwise we might leave orphans around
        // don't just restrict to the attachments.
        final List paths = new ArrayList();
        final Resource resource = resolver.getResource(path);
        if (resource == null) {
            return paths;
        }

        for (final Resource child : resource.getChildren()) {
            paths.addAll(getPaths(resolver, child.getPath()));
        }
        paths.add(path);

        return paths;
    }

    private String getUGCLimitModerators(String[] moderatorList) {
        StringBuilder strBuilder = new StringBuilder();
        if(moderatorList != null && moderatorList.length != 0) {
            strBuilder.append(moderatorList[0]);
            for (int i = 1; i < moderatorList.length; i++) {
                strBuilder.append(", " + moderatorList[i]);
            }
            return "Please contact :" + strBuilder.toString();
        } else {
            return "";
        }
    }

    private String getDurationString(final long duration) {
        final int second = 1000;
        final int minute = second * 60;
        final int hour = minute * 60;
        final long day = hour * 24;
        String durationString = "minute";
        if (duration < minute) {
            final long t = Math.round(duration / second);
            if (t == 1l) {
                durationString = "second";
            }
            durationString = "second";
        } else if (duration < hour) {
            final long t = Math.round(duration / minute);
            if (t == 1l) {
                durationString = "minute";
            }
            durationString = "minute";
        } else if (duration < day) {
            final long t = Math.round(duration / hour);
            if (t == 1l) {
                durationString = "hour";
            }
            durationString = "hour";
        } else {
            durationString = "day";
        }
        return durationString;
    }

    protected String getEntityUrl(final Resource resource) {
        return resource.adaptTo(CommentSystem.class).getUrl();
    }

    protected String getEventTopic() {
        return CommentEvent.COMMENT_TOPIC;
    }

    private static AtomicBoolean userManagerCompatibilityMessageLogged = new AtomicBoolean(false);

    /* deprecated as of 6.2 FP2/6.3 */
    @Deprecated
    protected void bindUserManagerFactory(final Object in) {
        // NOP for compatibility with concrete impls still binding obsolete com.day.cq.security.UserManagerFactory
        unbindUserManagerFactory(in);
    }

    /* deprecated as of 6.2 FP2/6.3 */
    @Deprecated
    protected void unbindUserManagerFactory(final Object in) {
        // NOP for compatibility with concrete impls still binding obsolete com.day.cq.security.UserManagerFactory
        if (!userManagerCompatibilityMessageLogged.getAndSet(true)) {
            LOG.info("Service binding for deprecated and unsupported UserManagerFactory encountered. "
                    + "Ensure all direct and indirect uses of AbstractCommentOperationService are recompiled with the latest applicable code.");
        }
    }

    /**
     * {@inheritDoc}
     */
    protected abstract S getSocialComponentForResource(final Resource resource);

    /**
     * Posts an OSGi create social event for this component.
     * @param component the component
     * @param authorId the person who is creating the content
     */
    protected abstract void postCreateEvent(S component, String authorId);

    /**
     * Posts an OSGi update social event for this component.
     * @param component the component
     * @param authorId the person who is updating the content
     */
    protected abstract void postUpdateEvent(S component, String authorId);

    /**
     * Posts an OSGi change state social event for this component.
     * @param component the component
     * @param authorId the person who is updating the content
     */
    protected abstract void postChangeStateEvent(S component, String authorId);

    /**
     * Posts an OSGi delete social event for this component.
     * @param component the component
     * @param authorId the person who is deleting the content
     */
    protected abstract void postDeleteEvent(S component, String authorId);

    protected abstract U getCreateOperation();

    protected abstract U getDeleteOperation();

    protected abstract U getUpdateOperation();

    protected abstract U getUploadImageOperation();

    protected abstract U getChangeStateOperation();

    protected abstract String getResourceType(final Resource root);

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy