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 <[email protected]> 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}