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.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 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.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.FileDataSource;
import com.adobe.cq.social.commons.FileUploadSizeLimit;
import com.adobe.cq.social.commons.comments.scheduler.api.ScheduledPostService;
import com.adobe.cq.social.commons.events.CommentEvent;
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.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.security.NoSuchAuthorizableException;
import com.day.cq.security.UserManagerFactory;
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";

    /**
     * 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";

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

    @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;
    /**
     * User Manager Factory.
     */
    @Reference
    protected UserManagerFactory userManagerFactory;

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

    @Reference
    protected EventAdmin eventAdmin;

    /**
     * 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
     * @param props
     * @param session
     * @throws RepositoryException
     * @throws OperationException
     */
    protected void getDefaultProperties(final SlingHttpServletRequest request, final Map props,
        final Session session) throws RepositoryException, OperationException {

        // date
        final Calendar cal = Calendar.getInstance();
        props.put(com.adobe.cq.social.commons.Comment.PROP_DATE, cal);

        // 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);

        // 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));
        }

        // 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 (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);
            }
        }

    }

    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 {
                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);
            }
        }

    }

    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 =
                CollabUtil.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();
    }

    /**
     * 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
     */
    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}.
     * @throws LoginException
     */
    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}.
     * @throws LoginException
     */
    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
     * @param session
     * @return the authorized id
     * @throws 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 = "anonymous".equals(sessionUserId);
            final boolean authorMode = isAuthorMode();
            if (!anonymous && authorMode) {
                final boolean userExists = userExists(userIdentifier, session);
                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
     * @return
     */
    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);
        }
        return CommentUtil.isUserPrivileged(cs.getResource(), resolver, userId);
    }

    /**
     * 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;
        } catch (final NoSuchAuthorizableException 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);
        }

        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);
            }
        }

        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;
                    }
                }
            }
            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);
        }

    }

    /**
     * @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 (!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);
        }

        // 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);
        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 (parent != null && !mayReply(targetCommentSystemResource, cs)) {
            throw new OperationException("Reply is not allowed: " + targetCommentSystemResource.getPath(),
                HttpServletResponse.SC_FORBIDDEN);
        }
        if (attachments.size() == 1) { // only support 1 attachment for now
            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.
                    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 ("anonymous".equals(userId)) {
                return false;
            }
            final SocialUtils socialUtils = resolver.adaptTo(SocialUtils.class);
            Resource r = resolver.getResource(cs.getPath());
            if (null != r) {
                r = resolver.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);
            }
            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
     */
    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 (!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);
        }

        final List attachments = getAttachmentsFromRequest(request, cs);
        return update(resource, cs, props, attachments, session, userId);
    }

    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);
            for (final Entry entry : props.entrySet()) {
                if (entry.getKey() == PROP_MESSAGE) {
                    final long messageCharacterLimit = cs.getMessageCharacterLimit();
                    final String message = CollabUtil.getValueString(entry.getValue());
                    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);

                    }

                    properties.put(com.adobe.cq.social.commons.Comment.PROP_MESSAGE, entry.getValue());
                } else {
                    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);
                }
            }
            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());
    }

    /**
     * Delete a {@link Comment} specified by the {@link SlingHttpServletRequest} using the specified {@link Session}
     * @param request
     * @param session
     * @throws OperationException
     */
    public void delete(final SlingHttpServletRequest request, final Session session) 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 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);
        }

        delete(reqResource, cs, session);

    }

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

    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);
        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(ValueMap.class);
            performBeforeActions(createOperation, session,
                resolver.getResource((String) props.get(SocialUtils.PN_CS_ROOT)), props);

            srp.commit(resolver);

            // perform after actions for create
            final S commentComp = getSocialComponentForResource(newResource);
            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);
        }
    }

    /**
     * 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
     * @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)
        throws OperationException {
        LOG.debug("delete for  {} ", commentResource.getPath());

        try {
            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());
            final List paths = getPaths(commentResource.getResourceResolver(), comment.getPath());
            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);
            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);
                }
            }
            comment.remove();
            comment.getCommentSystem().save();

            if (throwEvent) {
                postDeleteEvent(commentComp, author);
            }

            performAfterActions(deleteOperation, session, commentComp, Collections.emptyMap());
        } catch (final RepositoryException e) {
            throw new OperationException("Failed to delete comment", e, HttpServletResponse.SC_NOT_FOUND);
        }
    }

    /**
     * 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;
    }

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

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

    /**
     * {@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 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 String getResourceType(final Resource root);

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy