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