001/*
002 * (C) Copyright 2018 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 */
020
021package org.nuxeo.ecm.platform.comment.impl;
022
023import static java.util.Collections.singletonList;
024import static java.util.Collections.singletonMap;
025import static java.util.stream.Collectors.collectingAndThen;
026import static java.util.stream.Collectors.toList;
027import static org.nuxeo.ecm.platform.comment.api.ExternalEntityConstants.EXTERNAL_ENTITY_FACET;
028import static org.nuxeo.ecm.platform.comment.workflow.utils.CommentsConstants.COMMENT_ANCESTOR_IDS;
029import static org.nuxeo.ecm.platform.comment.workflow.utils.CommentsConstants.COMMENT_AUTHOR;
030import static org.nuxeo.ecm.platform.comment.workflow.utils.CommentsConstants.COMMENT_CREATION_DATE;
031import static org.nuxeo.ecm.platform.comment.workflow.utils.CommentsConstants.COMMENT_DOC_TYPE;
032import static org.nuxeo.ecm.platform.comment.workflow.utils.CommentsConstants.COMMENT_PARENT_ID;
033import static org.nuxeo.ecm.platform.comment.workflow.utils.CommentsConstants.COMMENT_SCHEMA;
034import static org.nuxeo.ecm.platform.query.nxql.CoreQueryAndFetchPageProvider.CORE_SESSION_PROPERTY;
035
036import java.io.Serializable;
037import java.time.Instant;
038import java.util.Collections;
039import java.util.List;
040import java.util.Map;
041
042import org.apache.commons.logging.Log;
043import org.apache.commons.logging.LogFactory;
044import org.nuxeo.ecm.core.api.CloseableCoreSession;
045import org.nuxeo.ecm.core.api.CoreInstance;
046import org.nuxeo.ecm.core.api.CoreSession;
047import org.nuxeo.ecm.core.api.DocumentModel;
048import org.nuxeo.ecm.core.api.DocumentRef;
049import org.nuxeo.ecm.core.api.IdRef;
050import org.nuxeo.ecm.core.api.NuxeoPrincipal;
051import org.nuxeo.ecm.core.api.PartialList;
052import org.nuxeo.ecm.core.api.PathRef;
053import org.nuxeo.ecm.core.api.SortInfo;
054import org.nuxeo.ecm.core.api.security.SecurityConstants;
055import org.nuxeo.ecm.platform.comment.api.Comment;
056import org.nuxeo.ecm.platform.comment.api.CommentEvents;
057import org.nuxeo.ecm.platform.comment.api.Comments;
058import org.nuxeo.ecm.platform.comment.api.ExternalEntity;
059import org.nuxeo.ecm.platform.comment.api.exceptions.CommentNotFoundException;
060import org.nuxeo.ecm.platform.comment.api.exceptions.CommentSecurityException;
061import org.nuxeo.ecm.platform.query.api.PageProvider;
062import org.nuxeo.ecm.platform.query.api.PageProviderService;
063import org.nuxeo.runtime.api.Framework;
064
065/**
066 * Comment service implementation. The comments are linked together thanks to a parent document id property.
067 * 
068 * @since 10.3
069 */
070public class PropertyCommentManager extends AbstractCommentManager {
071
072    private static final Log log = LogFactory.getLog(PropertyCommentManager.class);
073
074    protected static final String GET_COMMENT_PAGEPROVIDER_NAME = "GET_COMMENT_AS_EXTERNAL_ENTITY";
075
076    protected static final String GET_COMMENTS_FOR_DOC_PAGEPROVIDER_NAME = "GET_COMMENTS_FOR_DOCUMENT";
077
078    protected static final String HIDDEN_FOLDER_TYPE = "HiddenFolder";
079
080    protected static final String COMMENT_NAME = "comment";
081
082    @Override
083    @SuppressWarnings("unchecked")
084    public List<DocumentModel> getComments(CoreSession session, DocumentModel docModel)
085            throws CommentSecurityException {
086
087        DocumentRef docRef = getAncestorRef(session, docModel);
088
089        if (session.exists(docRef) && !session.hasPermission(docRef, SecurityConstants.READ)) {
090            throw new CommentSecurityException("The user " + session.getPrincipal().getName()
091                    + " does not have access to the comments of document " + docModel.getId());
092        }
093        PageProviderService ppService = Framework.getService(PageProviderService.class);
094        return CoreInstance.doPrivileged(session, s -> {
095            Map<String, Serializable> props = Collections.singletonMap(CORE_SESSION_PROPERTY, (Serializable) s);
096            PageProvider<DocumentModel> pageProvider = (PageProvider<DocumentModel>) ppService.getPageProvider(
097                    GET_COMMENTS_FOR_DOC_PAGEPROVIDER_NAME, singletonList(new SortInfo(COMMENT_CREATION_DATE, true)),
098                    null, null, props, docModel.getId());
099            return pageProvider.getCurrentPage();
100        });
101    }
102
103    @Override
104    public List<DocumentModel> getComments(DocumentModel docModel, DocumentModel parent) {
105        throw new UnsupportedOperationException("This service implementation does not implement deprecated API.");
106    }
107
108    @Override
109    public DocumentModel createComment(DocumentModel docModel, String comment) {
110        throw new UnsupportedOperationException("This service implementation does not implement deprecated API.");
111    }
112
113    @Override
114    public DocumentModel createComment(DocumentModel docModel, String text, String author) {
115        throw new UnsupportedOperationException("This service implementation does not implement deprecated API.");
116    }
117
118    @Override
119    public DocumentModel createComment(DocumentModel docModel, DocumentModel commentModel)
120            throws CommentSecurityException {
121
122        NuxeoPrincipal principal = commentModel.getCoreSession().getPrincipal();
123        // Open a session as system user since the parent document model can be a comment
124        try (CloseableCoreSession session = CoreInstance.openCoreSessionSystem(docModel.getRepositoryName())) {
125            DocumentRef docRef = getAncestorRef(session, docModel);
126            if (!session.hasPermission(principal, docRef, SecurityConstants.READ)) {
127                throw new CommentSecurityException(
128                        "The user " + principal.getName() + " can not create comments on document " + docModel.getId());
129            }
130
131            String path = getCommentContainerPath(session, docModel.getId());
132
133            DocumentModel commentModelToCreate = session.createDocumentModel(path, COMMENT_NAME,
134                    commentModel.getType());
135            commentModelToCreate.copyContent(commentModel);
136            commentModelToCreate.setPropertyValue(COMMENT_ANCESTOR_IDS,
137                    (Serializable) computeAncestorIds(session, docModel.getId()));
138            DocumentModel comment = session.createDocument(commentModelToCreate);
139            comment.detach(true);
140            notifyEvent(session, CommentEvents.COMMENT_ADDED, docModel, comment);
141            return comment;
142        }
143    }
144
145    @Override
146    public DocumentModel createComment(DocumentModel docModel, DocumentModel parent, DocumentModel child) {
147        throw new UnsupportedOperationException("This service implementation does not implement deprecated API.");
148    }
149
150    @Override
151    public void deleteComment(DocumentModel docModel, DocumentModel comment) {
152        throw new UnsupportedOperationException("This service implementation does not implement deprecated API.");
153    }
154
155    @Override
156    public List<DocumentModel> getDocumentsForComment(DocumentModel comment) {
157        throw new UnsupportedOperationException("This service implementation does not implement deprecated API.");
158    }
159
160    @Override
161    public DocumentModel getThreadForComment(DocumentModel comment) throws CommentSecurityException {
162        return getThreadForComment(comment.getCoreSession(), comment);
163    }
164
165    @Override
166    public DocumentModel createLocatedComment(DocumentModel docModel, DocumentModel comment, String path) {
167        CoreSession session = docModel.getCoreSession();
168        DocumentRef docRef = getAncestorRef(session, docModel);
169        if (!session.hasPermission(docRef, SecurityConstants.READ)) {
170            throw new CommentSecurityException("The user " + session.getPrincipal().getName()
171                    + " can not create comments on document " + docModel.getId());
172        }
173        return CoreInstance.doPrivileged(session, s -> {
174            DocumentModel commentModel = s.createDocumentModel(path, COMMENT_NAME, comment.getType());
175            commentModel.copyContent(comment);
176            commentModel.setPropertyValue(COMMENT_ANCESTOR_IDS, (Serializable) computeAncestorIds(s, docModel.getId()));
177            commentModel = s.createDocument(commentModel);
178            notifyEvent(s, CommentEvents.COMMENT_ADDED, docModel, commentModel);
179            return commentModel;
180        });
181    }
182
183    @Override
184    public Comment createComment(CoreSession session, Comment comment)
185            throws CommentNotFoundException, CommentSecurityException {
186        String parentId = comment.getParentId();
187        DocumentRef docRef = new IdRef(parentId);
188        // Parent document can be a comment, check existence as a privileged user
189        if (!CoreInstance.doPrivileged(session, s -> {return s.exists(docRef);})) {
190            throw new CommentNotFoundException("The document or comment " + comment.getParentId() + " does not exist.");
191        }
192        DocumentRef ancestorRef = CoreInstance.doPrivileged(session, s -> {
193            return getAncestorRef(s, s.getDocument(new IdRef(parentId)));
194        });
195        if (!session.hasPermission(ancestorRef, SecurityConstants.READ)) {
196            throw new CommentSecurityException("The user " + session.getPrincipal().getName()
197                    + " can not create comments on document " + parentId);
198        }
199
200        // Initiate Creation Date if it is not done yet
201        if (comment.getCreationDate() == null) {
202            comment.setCreationDate(Instant.now());
203        }
204
205        return CoreInstance.doPrivileged(session, s -> {
206            String path = getCommentContainerPath(s, parentId);
207            DocumentModel commentModel = s.createDocumentModel(path, COMMENT_NAME, COMMENT_DOC_TYPE);
208            Comments.commentToDocumentModel(comment, commentModel);
209            if (comment instanceof ExternalEntity) {
210                commentModel.addFacet(EXTERNAL_ENTITY_FACET);
211                Comments.externalEntityToDocumentModel((ExternalEntity) comment, commentModel);
212            }
213
214            // Compute the list of ancestor ids
215            commentModel.setPropertyValue(COMMENT_ANCESTOR_IDS, (Serializable) computeAncestorIds(s, parentId));
216            commentModel = s.createDocument(commentModel);
217            notifyEvent(s, CommentEvents.COMMENT_ADDED, s.getDocument(docRef), commentModel);
218            return Comments.newComment(commentModel);
219        });
220    }
221
222    @Override
223    public Comment getComment(CoreSession session, String commentId)
224            throws CommentNotFoundException, CommentSecurityException {
225        DocumentRef commentRef = new IdRef(commentId);
226        // Parent document can be a comment, check existence as a privileged user
227        if (!CoreInstance.doPrivileged(session, s -> {return s.exists(commentRef);})) {
228            throw new CommentNotFoundException("The comment " + commentId + " does not exist.");
229        }
230        NuxeoPrincipal principal = session.getPrincipal();
231        return CoreInstance.doPrivileged(session, s -> {
232            DocumentModel commentModel = s.getDocument(commentRef);
233            DocumentRef documentRef = getAncestorRef(s, commentModel);
234            if (!s.hasPermission(principal, documentRef, SecurityConstants.READ)) {
235                throw new CommentSecurityException("The user " + principal.getName()
236                        + " does not have access to the comments of document " + documentRef.reference());
237            }
238
239            return Comments.newComment(commentModel);
240        });
241    }
242
243    @Override
244    @SuppressWarnings("unchecked")
245    public PartialList<Comment> getComments(CoreSession session, String documentId, Long pageSize,
246            Long currentPageIndex, boolean sortAscending) throws CommentSecurityException {
247        DocumentRef docRef = new IdRef(documentId);
248        PageProviderService ppService = Framework.getService(PageProviderService.class);
249        NuxeoPrincipal principal = session.getPrincipal();
250        return CoreInstance.doPrivileged(session, s -> {
251            if (s.exists(docRef)) {
252                DocumentRef ancestorRef = getAncestorRef(s, s.getDocument(docRef));
253                if (s.exists(ancestorRef) && !s.hasPermission(principal, ancestorRef, SecurityConstants.READ)) {
254                    throw new CommentSecurityException("The user " + principal.getName()
255                            + " does not have access to the comments of document " + documentId);
256                }
257            }
258            Map<String, Serializable> props = Collections.singletonMap(CORE_SESSION_PROPERTY, (Serializable) s);
259            PageProvider<DocumentModel> pageProvider = (PageProvider<DocumentModel>) ppService.getPageProvider(
260                    GET_COMMENTS_FOR_DOC_PAGEPROVIDER_NAME,
261                    singletonList(new SortInfo(COMMENT_CREATION_DATE, sortAscending)), pageSize, currentPageIndex,
262                    props, documentId);
263            List<DocumentModel> commentList = pageProvider.getCurrentPage();
264            return commentList.stream()
265                              .map(Comments::newComment)
266                              .collect(collectingAndThen(toList(),
267                                      list -> new PartialList<>(list, pageProvider.getResultsCount())));
268        });
269    }
270
271    @Override
272    public Comment updateComment(CoreSession session, String commentId, Comment comment)
273            throws CommentNotFoundException {
274        IdRef commentRef = new IdRef(commentId);
275        if (!CoreInstance.doPrivileged(session, s -> {return s.exists(commentRef);})) {
276            throw new CommentNotFoundException("The comment " + commentId + " does not exist.");
277        }
278        NuxeoPrincipal principal = session.getPrincipal();
279        if (!principal.isAdministrator() && !comment.getAuthor().equals(principal.getName())) {
280            throw new CommentSecurityException(
281                    "The user " + principal.getName() + " can not edit comments of document " + comment.getParentId());
282        }
283        return CoreInstance.doPrivileged(session, s -> {
284            // Initiate Modification Date if it is not done yet
285            if (comment.getModificationDate() == null) {
286                comment.setModificationDate(Instant.now());
287            }
288
289            DocumentModel commentModel = s.getDocument(commentRef);
290            Comments.commentToDocumentModel(comment, commentModel);
291            if (comment instanceof ExternalEntity) {
292                Comments.externalEntityToDocumentModel((ExternalEntity) comment, commentModel);
293            }
294            s.saveDocument(commentModel);
295            return Comments.newComment(commentModel);
296        });
297    }
298
299    @Override
300    public void deleteComment(CoreSession session, String commentId)
301            throws CommentNotFoundException, CommentSecurityException {
302        IdRef commentRef = new IdRef(commentId);
303        // Document can be a comment, check existence as a privileged user
304        if (!CoreInstance.doPrivileged(session, s -> {return s.exists(commentRef);})) {
305            throw new CommentNotFoundException("The comment " + commentId + " does not exist.");
306        }
307
308        NuxeoPrincipal principal = session.getPrincipal();
309        CoreInstance.doPrivileged(session, s -> {
310            DocumentModel comment = s.getDocument(commentRef);
311            String parentId = (String) comment.getPropertyValue(COMMENT_PARENT_ID);
312            DocumentRef parentRef = new IdRef(parentId);
313            DocumentRef ancestorRef = getAncestorRef(s, comment);
314            if (s.exists(ancestorRef) && !principal.isAdministrator()
315                    && !comment.getPropertyValue(COMMENT_AUTHOR).equals(principal.getName())
316                    && !s.hasPermission(principal, ancestorRef, SecurityConstants.EVERYTHING)) {
317                throw new CommentSecurityException(
318                        "The user " + principal.getName() + " can not delete comments of document " + parentId);
319            }
320            DocumentModel parent = s.getDocument(parentRef);
321            s.removeDocument(commentRef);
322            notifyEvent(s, CommentEvents.COMMENT_REMOVED, parent, comment);
323        });
324    }
325
326    @Override
327    public Comment getExternalComment(CoreSession session, String entityId) throws CommentNotFoundException {
328        DocumentModel commentModel = getExternalCommentModel(session, entityId);
329        if (commentModel == null) {
330            throw new CommentNotFoundException("The external comment " + entityId + " does not exist.");
331        }
332        String parentId = (String) commentModel.getPropertyValue(COMMENT_PARENT_ID);
333        if (!session.hasPermission(getAncestorRef(session, commentModel), SecurityConstants.READ)) {
334            throw new CommentSecurityException("The user " + session.getPrincipal().getName()
335                    + " does not have access to the comments of document " + parentId);
336        }
337        return Framework.doPrivileged(() -> Comments.newComment(commentModel));
338    }
339
340    @Override
341    public Comment updateExternalComment(CoreSession session, String entityId, Comment comment)
342            throws CommentNotFoundException {
343        DocumentModel commentModel = getExternalCommentModel(session, entityId);
344        if (commentModel == null) {
345            throw new CommentNotFoundException("The external comment " + entityId + " does not exist.");
346        }
347        NuxeoPrincipal principal = session.getPrincipal();
348        if (!principal.isAdministrator() && !comment.getAuthor().equals(principal.getName())) {
349            throw new CommentSecurityException(
350                    "The user " + principal.getName() + " can not edit comments of document " + comment.getParentId());
351        }
352        return CoreInstance.doPrivileged(session, s -> {
353            Comments.commentToDocumentModel(comment, commentModel);
354            if (comment instanceof ExternalEntity) {
355                Comments.externalEntityToDocumentModel((ExternalEntity) comment, commentModel);
356            }
357            s.saveDocument(commentModel);
358            return Comments.newComment(commentModel);
359        });
360    }
361
362    @Override
363    public void deleteExternalComment(CoreSession session, String entityId) throws CommentNotFoundException {
364        DocumentModel commentModel = getExternalCommentModel(session, entityId);
365        if (commentModel == null) {
366            throw new CommentNotFoundException("The external comment " + entityId + " does not exist.");
367        }
368        NuxeoPrincipal principal = session.getPrincipal();
369        String parentId = (String) commentModel.getPropertyValue(COMMENT_PARENT_ID);
370        if (!principal.isAdministrator() && !commentModel.getPropertyValue(COMMENT_AUTHOR).equals(principal.getName())
371                && !session.hasPermission(principal, getAncestorRef(session, commentModel),
372                        SecurityConstants.EVERYTHING)) {
373            throw new CommentSecurityException(
374                    "The user " + principal.getName() + " can not delete comments of document " + parentId);
375        }
376        CoreInstance.doPrivileged(session, s -> {
377            DocumentModel comment = s.getDocument(commentModel.getRef());
378            DocumentModel parent = s.getDocument(new IdRef((String) comment.getPropertyValue(COMMENT_PARENT_ID)));
379            s.removeDocument(commentModel.getRef());
380            notifyEvent(s, CommentEvents.COMMENT_REMOVED, parent, comment);
381        });
382    }
383
384    @Override
385    public boolean hasFeature(Feature feature) {
386        switch (feature) {
387        case COMMENTS_LINKED_WITH_PROPERTY:
388            return true;
389        default:
390            throw new UnsupportedOperationException(feature.name());
391        }
392    }
393
394    @SuppressWarnings("unchecked")
395    protected DocumentModel getExternalCommentModel(CoreSession session, String entityId) {
396        PageProviderService ppService = Framework.getService(PageProviderService.class);
397        Map<String, Serializable> props = singletonMap(CORE_SESSION_PROPERTY, (Serializable) session);
398        List<DocumentModel> results = ((PageProvider<DocumentModel>) ppService.getPageProvider(
399                GET_COMMENT_PAGEPROVIDER_NAME, null, 1L, 0L, props, entityId)).getCurrentPage();
400        if (results.isEmpty()) {
401            return null;
402        }
403        return results.get(0);
404    }
405
406    protected String getCommentContainerPath(CoreSession session, String commentedDocumentId) {
407        return CoreInstance.doPrivileged(session, s -> {
408            // Create or retrieve the folder to store the comment.
409            // If the document is under a domain, the folder is a child of this domain.
410            // Otherwise, it is a child of the root document.
411            DocumentModel annotatedDoc = s.getDocument(new IdRef(commentedDocumentId));
412            String parentPath = "/";
413            if (annotatedDoc.getPath().segmentCount() > 1) {
414                parentPath += annotatedDoc.getPath().segment(0);
415            }
416            PathRef ref = new PathRef(parentPath, COMMENTS_DIRECTORY);
417            DocumentModel commentFolderDoc = s.createDocumentModel(parentPath, COMMENTS_DIRECTORY, HIDDEN_FOLDER_TYPE);
418            s.getOrCreateDocument(commentFolderDoc);
419            s.save();
420            return ref.toString();
421        });
422    }
423
424    protected DocumentRef getAncestorRef(CoreSession session, DocumentModel documentModel) {
425        return CoreInstance.doPrivileged(session, s -> {
426            if (!documentModel.hasSchema(COMMENT_SCHEMA)) {
427                return documentModel.getRef();
428            }
429            DocumentModel ancestorComment = getThreadForComment(s, documentModel);
430            return new IdRef((String) ancestorComment.getPropertyValue(COMMENT_PARENT_ID));
431        });
432    }
433
434    protected DocumentModel getThreadForComment(CoreSession session, DocumentModel comment)
435            throws CommentSecurityException {
436
437        NuxeoPrincipal principal = session.getPrincipal();
438        return CoreInstance.doPrivileged(session, s -> {
439            DocumentModel thread = comment;
440            DocumentModel parent = s.getDocument(new IdRef((String) thread.getPropertyValue(COMMENT_PARENT_ID)));
441            if (parent.hasSchema(COMMENT_SCHEMA)) {
442                thread = getThreadForComment(parent);
443            }
444            DocumentRef ancestorRef = s.getDocument(new IdRef((String) thread.getPropertyValue(COMMENT_PARENT_ID)))
445                                       .getRef();
446            if (!s.hasPermission(principal, ancestorRef, SecurityConstants.READ)) {
447                throw new CommentSecurityException("The user " + principal.getName()
448                        + " does not have access to the comments of document " + ancestorRef.reference());
449            }
450            return thread;
451        });
452    }
453}