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