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 * @return the created tags 211 */ 212 protected List<Map<String, Object>> _handleTags(TaggableAmetysObject taggableAmetysObject, List<Object> tags) 213 { 214 return _workspaceHelper.handleTags(taggableAmetysObject, tags); 215 } 216 217 /** 218 * Edit the attachments of an AttachableAmetysObject 219 * @param attachableAmetysObject the ametys object to edit 220 * @param newFiles list of new files 221 * @param newFileNames list of new files names 222 * @param deleteFiles list of names of old files to delete 223 */ 224 protected void _setAttachments(AttachableAmetysObject attachableAmetysObject, List<Part> newFiles, List<String> newFileNames, List<String> deleteFiles) 225 { 226 if (!newFiles.isEmpty() || !deleteFiles.isEmpty()) 227 { 228 List<Binary> attachments = attachableAmetysObject.getAttachments() 229 .stream() 230 .filter(b -> !deleteFiles.contains(b.getName())) 231 .collect(Collectors.toList()); 232 233 List<String> fileNames = attachments.stream() 234 .map(Binary::getFilename) 235 .collect(Collectors.toList()); 236 237 int i = 0; 238 for (Part newPart : newFiles) 239 { 240 String newName = newFileNames.get(i); 241 fileNames.add(newName); 242 Binary newBinary = _partToBinary(newPart, newName); 243 if (newBinary != null) 244 { 245 attachments.add(newBinary); 246 } 247 i++; 248 } 249 attachableAmetysObject.setAttachments(attachments); 250 } 251 } 252 253 private Binary _partToBinary(Part part, String name) 254 { 255 if (part.isRejected()) 256 { 257 getLogger().error("Part {} will not be uploaded because it's rejected", part.getFileName()); 258 return null; 259 } 260 261 try (InputStream is = part.getInputStream()) 262 { 263 Binary binary = new Binary(); 264 265 binary.setFilename(name); 266 binary.setInputStream(is); 267 binary.setLastModificationDate(ZonedDateTime.now()); 268 binary.setMimeType(part.getMimeType()); 269 270 return binary; 271 } 272 catch (Exception e) 273 { 274 getLogger().error("An error occurred getting binary from part {}", part.getFileName(), e); 275 } 276 277 return null; 278 } 279 280 /** 281 * Comment a commentableAmetysObject 282 * @param <T> type of the value to retrieve 283 * @param commentableAmetysObject the commentableAmetysObject 284 * @param commentText the comment text 285 * @param moduleRoot the module root 286 * @return The commentableAmetysObject 287 */ 288 public <T extends AbstractComment> T createComment(CommentableAmetysObject<T> commentableAmetysObject, String commentText, ModifiableTraversableAmetysObject moduleRoot) 289 { 290 UserIdentity userIdentity = _currentUserProvider.getUser(); 291 292 T comment = commentableAmetysObject.createComment(); 293 294 _setComment(comment, userIdentity, commentText); 295 296 moduleRoot.saveChanges(); 297 298 return comment; 299 } 300 301 /** 302 * Edit a commentableAmetysObject comment 303 * @param commentableAmetysObject the commentableAmetysObject 304 * @param commentId the comment Id 305 * @param commentText the comment text 306 * @param moduleRoot the module root 307 * @return The commentableAmetysObject 308 */ 309 public CommentableAmetysObject editComment(CommentableAmetysObject commentableAmetysObject, String commentId, String commentText, ModifiableTraversableAmetysObject moduleRoot) 310 { 311 UserIdentity userIdentity = _currentUserProvider.getUser(); 312 User user = _userManager.getUser(userIdentity); 313 AbstractComment comment = commentableAmetysObject.getComment(commentId); 314 String authorEmail = comment.getAuthorEmail(); 315 if (!authorEmail.equals(user.getEmail())) 316 { 317 throw new AccessDeniedException("User '" + userIdentity + "' tried to edit an other user's comment"); 318 } 319 320 if (comment.getContent().equals(commentText)) 321 { 322 return commentableAmetysObject; 323 } 324 325 comment.setContent(commentText); 326 if (comment instanceof RichTextComment richTextComment) 327 { 328 _setRichTextContent(commentText, richTextComment); 329 } 330 comment.setEdited(true); 331 332 moduleRoot.saveChanges(); 333 return commentableAmetysObject; 334 } 335 336 private void _setRichTextContent(String commentText, RichTextComment richTextComment) 337 { 338 RichText richText = richTextComment.getRichTextContent(); 339 if (richText == null) 340 { 341 richText = new RichText(); 342 } 343 344 try 345 { 346 _richTextTransformer.transform(commentText, richText); 347 } 348 catch (AmetysRepositoryException | IOException e) 349 { 350 throw new AmetysRepositoryException("Unable to transform the text " + commentText + " into a rich text for comment " + richTextComment.getId(), e); 351 } 352 353 richTextComment.setRichTextContent(richText); 354 } 355 356 /** 357 * Edit a commentableAmetysObject comment 358 * @param commentableAmetysObject the commentableAmetysObject 359 * @param commentId the comment Id 360 * @param moduleRoot the module root 361 * @return The commentableAmetysObject 362 */ 363 public CommentableAmetysObject deleteComment(CommentableAmetysObject commentableAmetysObject, String commentId, ModifiableTraversableAmetysObject moduleRoot) 364 { 365 AbstractComment comment = commentableAmetysObject.getComment(commentId); 366 367 if (comment.isSubComment()) 368 { 369 AbstractComment parentComment = comment.getCommentParent(); 370 List<AbstractComment> subComments = parentComment.getSubComment(true, true); 371 boolean hasAfterSubComments = _hasAfterSubComments(comment, subComments); 372 if (comment.hasSubComments() || hasAfterSubComments) 373 { 374 comment.setDeleted(true); 375 comment.setAccepted(false); 376 } 377 else 378 { 379 comment.remove(); 380 } 381 382 // Sort comment by creation date (Recent creation date in first) 383 List<AbstractComment> currentSubComments = parentComment.getSubComment(true, true); 384 Collections.sort(currentSubComments, (c1, c2) -> 385 { 386 return c2.getCreationDate().compareTo(c1.getCreationDate()); 387 }); 388 389 // Remove already deleted sub comment if no recent sub comment is present 390 for (AbstractComment subCom : currentSubComments) 391 { 392 if (subCom.isDeleted() && !subCom.hasSubComments()) 393 { 394 subCom.remove(); 395 } 396 else 397 { 398 break; 399 } 400 } 401 402 // Remove parent comment 403 if (parentComment.isDeleted()) 404 { 405 deleteComment(commentableAmetysObject, parentComment.getId(), moduleRoot); 406 } 407 } 408 else 409 { 410 List<AbstractComment> subComment = comment.getSubComment(true, true); 411 boolean hasSubComments = subComment.stream() 412 .filter(c -> !c.isDeleted()) // Ignore alreay deleted sub comment 413 .findAny() 414 .isPresent(); 415 if (hasSubComments) 416 { 417 comment.setDeleted(true); 418 comment.setAccepted(false); 419 } 420 else 421 { 422 comment.remove(); 423 } 424 } 425 426 moduleRoot.saveChanges(); 427 428 return commentableAmetysObject; 429 } 430 431 /** 432 * Check if a given comment should be deleted or removed 433 * @param comment the comment 434 * @param subComments the subcomments 435 * @return true if the comment should be deleted, false if the comment should be removed instead 436 */ 437 protected boolean _hasAfterSubComments(AbstractComment comment, List<AbstractComment> subComments) 438 { 439 boolean hasAfterSubComments = subComments.stream() 440 .filter(c -> !c.getId().equals(comment.getId())) //Don't take current sub comment 441 .filter(c -> !c.isDeleted()) // Ignore alreay deleted sub comment 442 .filter(c -> c.getCreationDate().isAfter(comment.getCreationDate())) // Just take sub comment after current comment 443 .findAny() 444 .isPresent(); 445 return hasAfterSubComments; 446 } 447 448 /** 449 * Answer to a commentableAmetysObject comment 450 * @param <T> type of the value to retrieve 451 * @param commentableAmetysObject the commentableAmetysObject 452 * @param commentId the parent comment Id 453 * @param commentText the comment text 454 * @param moduleRoot the module root 455 * @return The commentableAmetysObject 456 */ 457 public <T extends AbstractComment> T answerComment(CommentableAmetysObject<T> commentableAmetysObject, String commentId, String commentText, ModifiableTraversableAmetysObject moduleRoot) 458 { 459 AbstractComment comment = commentableAmetysObject.getComment(commentId); 460 461 UserIdentity userIdentity = _currentUserProvider.getUser(); 462 T subComment = comment.createSubComment(); 463 464 _setComment(subComment, userIdentity, commentText); 465 466 moduleRoot.saveChanges(); 467 return subComment; 468 } 469 470 private void _setComment(AbstractComment comment, UserIdentity userIdentity, String commentText) 471 { 472 User user = _userManager.getUser(userIdentity); 473 comment.setAuthorName(user.getFullName()); 474 comment.setAuthorEmail(user.getEmail()); 475 comment.setAuthor(userIdentity); 476 comment.setEmailHiddenStatus(true); 477 comment.setContent(commentText); 478 if (comment instanceof RichTextComment richTextComment) 479 { 480 _setRichTextContent(commentText, richTextComment); 481 } 482 comment.setValidated(true); 483 } 484 485 /** 486 * Answer to a commentableAmetysObject comment 487 * @param commentableAmetysObject the commentableAmetysObject 488 * @param commentId the parent comment Id 489 * @param liked true if the comment is liked, otherwise the comment is unliked 490 * @param moduleRoot the module root 491 * @return The commentableAmetysObject 492 */ 493 public CommentableAmetysObject likeOrUnlikeComment(CommentableAmetysObject commentableAmetysObject, String commentId, Boolean liked, ModifiableTraversableAmetysObject moduleRoot) 494 { 495 AbstractComment comment = commentableAmetysObject.getComment(commentId); 496 497 UserIdentity user = _currentUserProvider.getUser(); 498 if (Boolean.FALSE.equals(liked) 499 || liked == null && comment.getReactionUsers(ReactionType.LIKE).contains(user)) 500 { 501 comment.removeReaction(user, ReactionType.LIKE); 502 } 503 else 504 { 505 comment.addReaction(user, ReactionType.LIKE); 506 } 507 508 moduleRoot.saveChanges(); 509 return commentableAmetysObject; 510 } 511}