001/* 002 * Copyright 2022 Anyware Services 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 */ 016package org.ametys.plugins.workspaces; 017 018import java.io.IOException; 019import java.io.InputStream; 020import java.time.ZonedDateTime; 021import java.util.Collections; 022import java.util.List; 023import java.util.Map; 024import java.util.stream.Collectors; 025 026import org.apache.avalon.framework.component.Component; 027import org.apache.avalon.framework.context.Context; 028import org.apache.avalon.framework.context.ContextException; 029import org.apache.avalon.framework.context.Contextualizable; 030import org.apache.avalon.framework.service.ServiceException; 031import org.apache.avalon.framework.service.ServiceManager; 032import org.apache.avalon.framework.service.Serviceable; 033import org.apache.cocoon.components.ContextHelper; 034import org.apache.cocoon.environment.Request; 035import org.apache.cocoon.servlet.multipart.Part; 036 037import org.ametys.cms.data.Binary; 038import org.ametys.cms.data.RichText; 039import org.ametys.cms.repository.AttachableAmetysObject; 040import org.ametys.cms.repository.CommentableAmetysObject; 041import org.ametys.cms.repository.ReactionableObject.ReactionType; 042import org.ametys.cms.repository.comment.AbstractComment; 043import org.ametys.cms.repository.comment.RichTextComment; 044import org.ametys.cms.transformation.RichTextTransformer; 045import org.ametys.cms.transformation.docbook.DocbookTransformer; 046import org.ametys.core.observation.ObservationManager; 047import org.ametys.core.right.RightManager; 048import org.ametys.core.right.RightManager.RightResult; 049import org.ametys.core.user.CurrentUserProvider; 050import org.ametys.core.user.User; 051import org.ametys.core.user.UserIdentity; 052import org.ametys.core.user.UserManager; 053import org.ametys.plugins.repository.AmetysObject; 054import org.ametys.plugins.repository.AmetysObjectResolver; 055import org.ametys.plugins.repository.AmetysRepositoryException; 056import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 057import org.ametys.plugins.repository.tag.TaggableAmetysObject; 058import org.ametys.plugins.workflow.support.WorkflowHelper; 059import org.ametys.plugins.workflow.support.WorkflowProvider; 060import org.ametys.plugins.workspaces.project.ProjectManager; 061import org.ametys.plugins.workspaces.project.modules.WorkspaceModuleExtensionPoint; 062import org.ametys.plugins.workspaces.project.objects.Project; 063import org.ametys.plugins.workspaces.tags.ProjectTagsDAO; 064import org.ametys.runtime.plugin.component.AbstractLogEnabled; 065 066/** 067 * Abstract class for workspace modules DAO's 068 * 069 */ 070public abstract class AbstractWorkspaceDAO extends AbstractLogEnabled implements Serviceable, Component, Contextualizable 071{ 072 073 /** Ametys resolver */ 074 protected AmetysObjectResolver _resolver; 075 076 /** Observer manager. */ 077 protected ObservationManager _observationManager; 078 079 /** The current user provider. */ 080 protected CurrentUserProvider _currentUserProvider; 081 082 /** The rights manager */ 083 protected RightManager _rightManager; 084 085 /** User manager */ 086 protected UserManager _userManager; 087 088 /** The workflow provider */ 089 protected WorkflowProvider _workflowProvider; 090 091 /** The workflow helper */ 092 protected WorkflowHelper _workflowHelper; 093 094 /** Workspaces project manager */ 095 protected ProjectManager _projectManager; 096 097 /** The avalon context */ 098 protected Context _context; 099 100 /** The workspace module EP */ 101 protected WorkspaceModuleExtensionPoint _workspaceModuleEP; 102 103 /** The project tags DAO */ 104 protected ProjectTagsDAO _projectTagsDAO; 105 106 /** The Workspaces helper */ 107 protected WorkspacesHelper _workspaceHelper; 108 /** Rich text transformer */ 109 protected RichTextTransformer _richTextTransformer; 110 111 public void contextualize(Context context) throws ContextException 112 { 113 _context = context; 114 } 115 116 public void service(ServiceManager manager) throws ServiceException 117 { 118 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 119 _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE); 120 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 121 _rightManager = (RightManager) manager.lookup(RightManager.ROLE); 122 _userManager = (UserManager) manager.lookup(UserManager.ROLE); 123 _workflowProvider = (WorkflowProvider) manager.lookup(WorkflowProvider.ROLE); 124 _workflowHelper = (WorkflowHelper) manager.lookup(WorkflowHelper.ROLE); 125 _projectManager = (ProjectManager) manager.lookup(ProjectManager.ROLE); 126 _workspaceModuleEP = (WorkspaceModuleExtensionPoint) manager.lookup(WorkspaceModuleExtensionPoint.ROLE); 127 _projectTagsDAO = (ProjectTagsDAO) manager.lookup(ProjectTagsDAO.ROLE); 128 _workspaceHelper = (WorkspacesHelper) manager.lookup(WorkspacesHelper.ROLE); 129 _richTextTransformer = (RichTextTransformer) manager.lookup(DocbookTransformer.ROLE); 130 } 131 132 /** 133 * Get the project name 134 * @return the project name 135 */ 136 protected String _getProjectName() 137 { 138 Request request = ContextHelper.getRequest(_context); 139 return (String) request.getAttribute("projectName"); 140 } 141 142 /** 143 * Get the project 144 * @return the project 145 */ 146 protected Project _getProject() 147 { 148 return _projectManager.getProject(_getProjectName()); 149 } 150 151 /** 152 * Get the sitemap language 153 * @return the sitemap language 154 */ 155 protected String _getSitemapLanguage() 156 { 157 Request request = ContextHelper.getRequest(_context); 158 return (String) request.getAttribute("sitemapLanguage"); 159 } 160 161 /** 162 * Get the site name 163 * @return the site name 164 */ 165 protected String _getSiteName() 166 { 167 Request request = ContextHelper.getRequest(_context); 168 return (String) request.getAttribute("siteName"); 169 } 170 171 /** 172 * Check user rights 173 * @param objectToCheck the object to check 174 * @param rightId the right id 175 * @throws IllegalAccessException if a right error occurred 176 */ 177 protected void _checkUserRights(AmetysObject objectToCheck, String rightId) throws IllegalAccessException 178 { 179 if (_rightManager.hasRight(_currentUserProvider.getUser(), rightId, objectToCheck) != RightResult.RIGHT_ALLOW) 180 { 181 throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to do operation without convenient right [" + rightId + "]"); 182 } 183 } 184 185 /** 186 * Check user reading rights 187 * @param objectToCheck the object to check 188 * @throws IllegalAccessException if a right error occurred 189 */ 190 protected void _checkUserReadingRights(AmetysObject objectToCheck) throws IllegalAccessException 191 { 192 if (!_rightManager.currentUserHasReadAccess(objectToCheck)) 193 { 194 throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to do read operation without convenient right"); 195 } 196 } 197 198 /** 199 * Handle tags for the edition of a TaggableAmetysObject 200 * @param taggableAmetysObject the object to edit 201 * @param tags the tags 202 * @return the created tags 203 */ 204 protected List<Map<String, Object>> _handleTags(TaggableAmetysObject taggableAmetysObject, List<Object> tags) 205 { 206 return _workspaceHelper.handleTags(taggableAmetysObject, tags); 207 } 208 209 /** 210 * Edit the attachments of an AttachableAmetysObject 211 * @param attachableAmetysObject the ametys object to edit 212 * @param newFiles list of new files 213 * @param newFileNames list of new files names 214 * @param deleteFiles list of names of old files to delete 215 */ 216 protected void _setAttachments(AttachableAmetysObject attachableAmetysObject, List<Part> newFiles, List<String> newFileNames, List<String> deleteFiles) 217 { 218 if (!newFiles.isEmpty() || !deleteFiles.isEmpty()) 219 { 220 List<Binary> attachments = attachableAmetysObject.getAttachments() 221 .stream() 222 .filter(b -> !deleteFiles.contains(b.getName())) 223 .collect(Collectors.toList()); 224 225 List<String> fileNames = attachments.stream() 226 .map(Binary::getFilename) 227 .collect(Collectors.toList()); 228 229 int i = 0; 230 for (Part newPart : newFiles) 231 { 232 String newName = newFileNames.get(i); 233 fileNames.add(newName); 234 Binary newBinary = _partToBinary(newPart, newName); 235 if (newBinary != null) 236 { 237 attachments.add(newBinary); 238 } 239 i++; 240 } 241 attachableAmetysObject.setAttachments(attachments); 242 } 243 } 244 245 private Binary _partToBinary(Part part, String name) 246 { 247 if (part.isRejected()) 248 { 249 getLogger().error("Part {} will not be uploaded because it's rejected", part.getFileName()); 250 return null; 251 } 252 253 try (InputStream is = part.getInputStream()) 254 { 255 Binary binary = new Binary(); 256 257 binary.setFilename(name); 258 binary.setInputStream(is); 259 binary.setLastModificationDate(ZonedDateTime.now()); 260 binary.setMimeType(part.getMimeType()); 261 262 return binary; 263 } 264 catch (Exception e) 265 { 266 getLogger().error("An error occurred getting binary from part {}", part.getFileName(), e); 267 } 268 269 return null; 270 } 271 272 /** 273 * Comment a commentableAmetysObject 274 * @param <T> type of the value to retrieve 275 * @param commentableAmetysObject the commentableAmetysObject 276 * @param commentText the comment text 277 * @param moduleRoot the module root 278 * @return The commentableAmetysObject 279 */ 280 public <T extends AbstractComment> T createComment(CommentableAmetysObject<T> commentableAmetysObject, String commentText, ModifiableTraversableAmetysObject moduleRoot) 281 { 282 UserIdentity userIdentity = _currentUserProvider.getUser(); 283 284 T comment = commentableAmetysObject.createComment(); 285 286 _setComment(comment, userIdentity, commentText); 287 288 moduleRoot.saveChanges(); 289 290 return comment; 291 } 292 293 /** 294 * Edit a commentableAmetysObject comment 295 * @param commentableAmetysObject the commentableAmetysObject 296 * @param commentId the comment Id 297 * @param commentText the comment text 298 * @param moduleRoot the module root 299 * @return The commentableAmetysObject 300 * @throws IllegalAccessException If an error occurs when checking the rights 301 */ 302 public CommentableAmetysObject editComment(CommentableAmetysObject commentableAmetysObject, String commentId, String commentText, ModifiableTraversableAmetysObject moduleRoot) throws IllegalAccessException 303 { 304 UserIdentity userIdentity = _currentUserProvider.getUser(); 305 User user = _userManager.getUser(userIdentity); 306 AbstractComment comment = commentableAmetysObject.getComment(commentId); 307 String authorEmail = comment.getAuthorEmail(); 308 if (!authorEmail.equals(user.getEmail())) 309 { 310 throw new IllegalAccessException("User '" + userIdentity + "' tried to edit an other user's comment"); 311 } 312 313 if (comment.getContent().equals(commentText)) 314 { 315 return commentableAmetysObject; 316 } 317 318 comment.setContent(commentText); 319 if (comment instanceof RichTextComment richTextComment) 320 { 321 _setRichTextContent(commentText, richTextComment); 322 } 323 comment.setEdited(true); 324 325 moduleRoot.saveChanges(); 326 return commentableAmetysObject; 327 } 328 329 private void _setRichTextContent(String commentText, RichTextComment richTextComment) 330 { 331 RichText richText = richTextComment.getRichTextContent(); 332 if (richText == null) 333 { 334 richText = new RichText(); 335 } 336 337 try 338 { 339 _richTextTransformer.transform(commentText, richText); 340 } 341 catch (AmetysRepositoryException | IOException e) 342 { 343 throw new AmetysRepositoryException("Unable to transform the text " + commentText + " into a rich text for comment " + richTextComment.getId(), e); 344 } 345 346 richTextComment.setRichTextContent(richText); 347 } 348 349 /** 350 * Edit a commentableAmetysObject comment 351 * @param commentableAmetysObject the commentableAmetysObject 352 * @param commentId the comment Id 353 * @param moduleRoot the module root 354 * @return The commentableAmetysObject 355 */ 356 public CommentableAmetysObject deleteComment(CommentableAmetysObject commentableAmetysObject, String commentId, ModifiableTraversableAmetysObject moduleRoot) 357 { 358 AbstractComment comment = commentableAmetysObject.getComment(commentId); 359 360 if (comment.isSubComment()) 361 { 362 AbstractComment parentComment = comment.getCommentParent(); 363 List<AbstractComment> subComments = parentComment.getSubComment(true, true); 364 boolean hasAfterSubComments = _hasAfterSubComments(comment, subComments); 365 if (comment.hasSubComments() || hasAfterSubComments) 366 { 367 comment.setDeleted(true); 368 comment.setAccepted(false); 369 } 370 else 371 { 372 comment.remove(); 373 } 374 375 // Sort comment by creation date (Recent creation date in first) 376 List<AbstractComment> currentSubComments = parentComment.getSubComment(true, true); 377 Collections.sort(currentSubComments, (c1, c2) -> 378 { 379 return c2.getCreationDate().compareTo(c1.getCreationDate()); 380 }); 381 382 // Remove already deleted sub comment if no recent sub comment is present 383 for (AbstractComment subCom : currentSubComments) 384 { 385 if (subCom.isDeleted() && !subCom.hasSubComments()) 386 { 387 subCom.remove(); 388 } 389 else 390 { 391 break; 392 } 393 } 394 395 // Remove parent comment 396 if (parentComment.isDeleted()) 397 { 398 deleteComment(commentableAmetysObject, parentComment.getId(), moduleRoot); 399 } 400 } 401 else 402 { 403 List<AbstractComment> subComment = comment.getSubComment(true, true); 404 boolean hasSubComments = subComment.stream() 405 .filter(c -> !c.isDeleted()) // Ignore alreay deleted sub comment 406 .findAny() 407 .isPresent(); 408 if (hasSubComments) 409 { 410 comment.setDeleted(true); 411 comment.setAccepted(false); 412 } 413 else 414 { 415 comment.remove(); 416 } 417 } 418 419 moduleRoot.saveChanges(); 420 421 return commentableAmetysObject; 422 } 423 424 /** 425 * Check if a given comment should be deleted or removed 426 * @param comment the comment 427 * @param subComments the subcomments 428 * @return true if the comment should be deleted, false if the comment should be removed instead 429 */ 430 protected boolean _hasAfterSubComments(AbstractComment comment, List<AbstractComment> subComments) 431 { 432 boolean hasAfterSubComments = subComments.stream() 433 .filter(c -> !c.getId().equals(comment.getId())) //Don't take current sub comment 434 .filter(c -> !c.isDeleted()) // Ignore alreay deleted sub comment 435 .filter(c -> c.getCreationDate().isAfter(comment.getCreationDate())) // Just take sub comment after current comment 436 .findAny() 437 .isPresent(); 438 return hasAfterSubComments; 439 } 440 441 /** 442 * Answer to a commentableAmetysObject comment 443 * @param <T> type of the value to retrieve 444 * @param commentableAmetysObject the commentableAmetysObject 445 * @param commentId the parent comment Id 446 * @param commentText the comment text 447 * @param moduleRoot the module root 448 * @return The commentableAmetysObject 449 */ 450 public <T extends AbstractComment> T answerComment(CommentableAmetysObject<T> commentableAmetysObject, String commentId, String commentText, ModifiableTraversableAmetysObject moduleRoot) 451 { 452 AbstractComment comment = commentableAmetysObject.getComment(commentId); 453 454 UserIdentity userIdentity = _currentUserProvider.getUser(); 455 T subComment = comment.createSubComment(); 456 457 _setComment(subComment, userIdentity, commentText); 458 459 moduleRoot.saveChanges(); 460 return subComment; 461 } 462 463 private void _setComment(AbstractComment comment, UserIdentity userIdentity, String commentText) 464 { 465 User user = _userManager.getUser(userIdentity); 466 comment.setAuthorName(user.getFullName()); 467 comment.setAuthorEmail(user.getEmail()); 468 comment.setAuthor(userIdentity); 469 comment.setEmailHiddenStatus(true); 470 comment.setContent(commentText); 471 if (comment instanceof RichTextComment richTextComment) 472 { 473 _setRichTextContent(commentText, richTextComment); 474 } 475 comment.setValidated(true); 476 } 477 478 /** 479 * Answer to a commentableAmetysObject comment 480 * @param commentableAmetysObject the commentableAmetysObject 481 * @param commentId the parent comment Id 482 * @param liked true if the comment is liked, otherwise the comment is unliked 483 * @param moduleRoot the module root 484 * @return The commentableAmetysObject 485 */ 486 public CommentableAmetysObject likeOrUnlikeComment(CommentableAmetysObject commentableAmetysObject, String commentId, Boolean liked, ModifiableTraversableAmetysObject moduleRoot) 487 { 488 AbstractComment comment = commentableAmetysObject.getComment(commentId); 489 490 UserIdentity user = _currentUserProvider.getUser(); 491 if (Boolean.FALSE.equals(liked) 492 || liked == null && comment.getReactionUsers(ReactionType.LIKE).contains(user)) 493 { 494 comment.removeReaction(user, ReactionType.LIKE); 495 } 496 else 497 { 498 comment.addReaction(user, ReactionType.LIKE); 499 } 500 501 moduleRoot.saveChanges(); 502 return commentableAmetysObject; 503 } 504}