001/*
002 * (C) Copyright 2018-2020 Nuxeo (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Funsho David
018 *     Nuno Cunha <ncunha@nuxeo.com>
019 *     Nour AL KOTOB
020 */
021
022package org.nuxeo.ecm.platform.comment.impl;
023
024import static java.util.Objects.requireNonNull;
025import static org.apache.commons.lang3.StringUtils.isBlank;
026import static org.nuxeo.ecm.platform.comment.api.CommentConstants.COMMENT_AUTHOR_PROPERTY;
027import static org.nuxeo.ecm.platform.comment.api.CommentConstants.COMMENT_PARENT_ID_PROPERTY;
028import static org.nuxeo.ecm.platform.comment.api.CommentConstants.COMMENT_SCHEMA;
029import static org.nuxeo.ecm.platform.comment.api.CommentConstants.COMMENT_TEXT_PROPERTY;
030
031import java.io.Serializable;
032import java.time.Instant;
033import java.util.HashMap;
034import java.util.HashSet;
035import java.util.List;
036import java.util.Map;
037import java.util.Set;
038
039import org.apache.commons.lang3.ArrayUtils;
040import org.apache.commons.lang3.StringUtils;
041import org.apache.logging.log4j.LogManager;
042import org.apache.logging.log4j.Logger;
043import org.nuxeo.ecm.core.api.CoreInstance;
044import org.nuxeo.ecm.core.api.CoreSession;
045import org.nuxeo.ecm.core.api.DocumentModel;
046import org.nuxeo.ecm.core.api.DocumentRef;
047import org.nuxeo.ecm.core.api.IdRef;
048import org.nuxeo.ecm.core.api.NuxeoPrincipal;
049import org.nuxeo.ecm.core.api.PartialList;
050import org.nuxeo.ecm.core.api.PropertyException;
051import org.nuxeo.ecm.core.api.security.ACE;
052import org.nuxeo.ecm.core.api.security.ACL;
053import org.nuxeo.ecm.core.api.security.ACP;
054import org.nuxeo.ecm.core.api.security.SecurityConstants;
055import org.nuxeo.ecm.core.api.security.impl.ACLImpl;
056import org.nuxeo.ecm.core.api.security.impl.ACPImpl;
057import org.nuxeo.ecm.core.event.Event;
058import org.nuxeo.ecm.core.event.EventProducer;
059import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
060import org.nuxeo.ecm.platform.comment.api.Comment;
061import org.nuxeo.ecm.platform.comment.api.CommentConstants;
062import org.nuxeo.ecm.platform.comment.api.CommentManager;
063import org.nuxeo.ecm.platform.comment.api.exceptions.CommentNotFoundException;
064import org.nuxeo.ecm.platform.comment.api.exceptions.CommentSecurityException;
065import org.nuxeo.ecm.platform.usermanager.UserManager;
066import org.nuxeo.runtime.api.Framework;
067
068/**
069 * @since 10.3
070 */
071public abstract class AbstractCommentManager implements CommentManager {
072
073    private static final Logger log = LogManager.getLogger(AbstractCommentManager.class);
074
075    /** @since 11.1 */
076    public static final String COMMENTS_DIRECTORY = "Comments";
077
078    @Override
079    public List<DocumentModel> getComments(DocumentModel docModel) {
080        return getComments(docModel.getCoreSession(), docModel);
081    }
082
083    @Override
084    @SuppressWarnings("removal")
085    public List<DocumentModel> getComments(DocumentModel docModel, DocumentModel parent) {
086        return getComments(docModel);
087    }
088
089    @Override
090    public List<Comment> getComments(CoreSession session, String documentId) {
091        return getComments(session, documentId, 0L, 0L, true);
092    }
093
094    @Override
095    public List<Comment> getComments(CoreSession session, String documentId, boolean sortAscending) {
096        return getComments(session, documentId, 0L, 0L, sortAscending);
097    }
098
099    @Override
100    public PartialList<Comment> getComments(CoreSession session, String documentId, Long pageSize,
101            Long currentPageIndex) {
102        return getComments(session, documentId, pageSize, currentPageIndex, true);
103    }
104
105    @Override
106    public DocumentRef getTopLevelDocumentRef(CoreSession session, DocumentRef commentRef) {
107        NuxeoPrincipal principal = session.getPrincipal();
108        return CoreInstance.doPrivileged(session, s -> {
109            if (!s.exists(commentRef)) {
110                throw new CommentNotFoundException(String.format("The comment %s does not exist.", commentRef));
111            }
112
113            DocumentModel commentDoc = s.getDocument(commentRef);
114            DocumentModel topLevelDoc = getTopLevelDocument(s, commentDoc);
115            DocumentRef topLevelDocRef = topLevelDoc.getRef();
116
117            if (!s.hasPermission(principal, topLevelDocRef, SecurityConstants.READ)) {
118                throw new CommentSecurityException("The user " + principal.getName()
119                        + " does not have access to the comments of document " + topLevelDocRef);
120            }
121
122            return topLevelDocRef;
123        });
124    }
125
126    /**
127     * Notifies the event of type {@code eventType} on the given {@code commentDoc}.
128     *
129     * @param session the session
130     * @param eventType the event type to fire
131     * @param commentDoc the document model of the comment
132     * @implSpec This method uses internally {@link #notifyEvent(CoreSession, String, DocumentModel, DocumentModel)}
133     * @since 11.1
134     */
135    protected void notifyEvent(CoreSession session, String eventType, DocumentModel commentDoc) {
136        DocumentModel commentedDoc = getCommentedDocument(session, commentDoc);
137        notifyEvent(session, eventType, commentedDoc, commentDoc);
138    }
139
140    protected void notifyEvent(CoreSession session, String eventType, DocumentModel commentedDoc,
141            DocumentModel commentDoc) {
142        DocumentModel topLevelDoc = getTopLevelDocument(session, commentDoc);
143        notifyEvent(session, eventType, topLevelDoc, commentedDoc, commentDoc);
144    }
145
146    /**
147     * @since 11.1
148     */
149    protected void notifyEvent(CoreSession session, String eventType, DocumentModel topLevelDoc,
150            DocumentModel commentedDoc, DocumentModel commentDoc) {
151        requireNonNull(eventType);
152        UserManager userManager = Framework.getService(UserManager.class);
153        NuxeoPrincipal principal = null;
154        if (userManager != null) {
155            principal = userManager.getPrincipal((String) commentDoc.getPropertyValue(COMMENT_AUTHOR_PROPERTY));
156            if (principal == null) {
157                try {
158                    principal = getAuthor(commentDoc);
159                } catch (PropertyException e) {
160                    log.error("Error building principal for comment author", e);
161                    return;
162                }
163            }
164        }
165        DocumentEventContext ctx = new DocumentEventContext(session, principal, commentedDoc);
166        Map<String, Serializable> props = new HashMap<>();
167        props.put(CommentConstants.TOP_LEVEL_DOCUMENT, topLevelDoc);
168        props.put(CommentConstants.PARENT_COMMENT, commentedDoc);
169        // simplifies template checks and vars expansion
170        if (!topLevelDoc.equals(commentedDoc)) {
171            String commentAuthor;
172            NuxeoPrincipal commentPrincipal = getAuthor(commentedDoc);
173            if (commentPrincipal != null) {
174                commentAuthor = commentPrincipal.getFirstName();
175                commentAuthor = isBlank(commentAuthor) ? commentPrincipal.getName() : commentAuthor;
176            } else {
177                commentAuthor = ((String[]) commentedDoc.getPropertyValue("dc:contributors"))[0];
178            }
179            props.put(CommentConstants.PARENT_COMMENT_AUTHOR, commentAuthor);
180        }
181        props.put(CommentConstants.COMMENT_DOCUMENT, commentDoc);
182        props.put(CommentConstants.COMMENT, commentDoc.getPropertyValue(COMMENT_TEXT_PROPERTY));
183        // Keep comment_text for compatibility
184        props.put(CommentConstants.COMMENT_TEXT, commentDoc.getPropertyValue(COMMENT_TEXT_PROPERTY));
185        props.put("category", CommentConstants.EVENT_COMMENT_CATEGORY);
186        ctx.setProperties(props);
187        Event event = ctx.newEvent(eventType);
188
189        EventProducer evtProducer = Framework.getService(EventProducer.class);
190        evtProducer.fireEvent(event);
191    }
192
193    protected abstract DocumentModel getTopLevelDocument(CoreSession session, DocumentModel commentDoc);
194
195    protected abstract DocumentModel getCommentedDocument(CoreSession session, DocumentModel commentDoc);
196
197    protected NuxeoPrincipal getAuthor(DocumentModel docModel) {
198        String author = null;
199        if (docModel.hasSchema(COMMENT_SCHEMA)) {
200            // means annotation / comment
201            author = (String) docModel.getPropertyValue(COMMENT_AUTHOR_PROPERTY);
202        }
203        if (StringUtils.isBlank(author)) {
204            String[] contributors = (String[]) docModel.getPropertyValue("dc:contributors");
205            if (ArrayUtils.isNotEmpty(contributors)) {
206                author = contributors[0];
207            }
208        }
209
210        NuxeoPrincipal principal = Framework.getService(UserManager.class).getPrincipal(author);
211        // If principal doesn't exist anymore
212        if (principal == null) {
213            log.debug("Principal not found: {}", author);
214        }
215        return principal;
216    }
217
218    protected void setFolderPermissions(CoreSession session, DocumentModel documentModel) {
219        ACP acp = documentModel.getACP();
220        acp.blockInheritance(ACL.LOCAL_ACL, SecurityConstants.SYSTEM_USERNAME);
221        documentModel.setACP(acp, true);
222    }
223
224    /**
225     * @deprecated since 11.1. Not used anymore
226     */
227    @Deprecated(since = "11.1")
228    protected void setCommentPermissions(CoreSession session, DocumentModel documentModel) {
229        ACP acp = new ACPImpl();
230        ACE grantRead = new ACE(SecurityConstants.EVERYONE, SecurityConstants.READ, true);
231        ACE grantRemove = new ACE("members", SecurityConstants.REMOVE, true);
232        ACL acl = new ACLImpl();
233        acl.setACEs(new ACE[] { grantRead, grantRemove });
234        acp.addACL(acl);
235        session.setACP(documentModel.getRef(), acp, true);
236    }
237
238    protected void fillCommentForCreation(CoreSession session, Comment comment) {
239        // Initiate Author if it is not done yet
240        if (comment.getAuthor() == null) {
241            comment.setAuthor(session.getPrincipal().getName());
242        }
243
244        // Initiate Creation Date if it is not done yet
245        if (comment.getCreationDate() == null) {
246            comment.setCreationDate(Instant.now());
247        }
248
249        // Initiate Modification Date if it is not done yet
250        if (comment.getModificationDate() == null) {
251            comment.setModificationDate(Instant.now());
252        }
253    }
254
255    /**
256     * @param session the session allowing to get parent documents, depending on implementation it should be privileged
257     */
258    @SuppressWarnings("unchecked")
259    protected <S extends Set<String> & Serializable> S computeAncestorIds(CoreSession session, String parentId) {
260        Set<String> ancestorIds = new HashSet<>();
261        ancestorIds.add(parentId);
262        DocumentRef parentRef = new IdRef(parentId);
263        while (session.exists(parentRef) && session.getDocument(parentRef).hasSchema(COMMENT_SCHEMA)) {
264            parentId = (String) session.getDocument(parentRef).getPropertyValue(COMMENT_PARENT_ID_PROPERTY);
265            ancestorIds.add(parentId);
266            parentRef = new IdRef(parentId);
267        }
268        return (S) ancestorIds;
269    }
270
271}