001/* 002 * Copyright 2020 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.cms.repository.comment; 017 018import java.util.ArrayList; 019import java.util.HashMap; 020import java.util.List; 021import java.util.Map; 022import java.util.Objects; 023import java.util.Optional; 024import java.util.Set; 025import java.util.regex.Pattern; 026 027import org.apache.avalon.framework.component.Component; 028import org.apache.avalon.framework.context.Context; 029import org.apache.avalon.framework.context.ContextException; 030import org.apache.avalon.framework.context.Contextualizable; 031import org.apache.avalon.framework.service.ServiceException; 032import org.apache.avalon.framework.service.ServiceManager; 033import org.apache.avalon.framework.service.Serviceable; 034import org.apache.cocoon.components.ContextHelper; 035import org.apache.cocoon.environment.Request; 036import org.apache.cocoon.xml.AttributesImpl; 037import org.apache.cocoon.xml.XMLUtils; 038import org.apache.commons.lang3.StringUtils; 039import org.apache.commons.lang3.Strings; 040import org.xml.sax.ContentHandler; 041import org.xml.sax.SAXException; 042 043import org.ametys.cms.ObservationConstants; 044import org.ametys.cms.fo.ForceDefaultRepositoryWorkspaceCallableDecorator; 045import org.ametys.cms.repository.Content; 046import org.ametys.cms.repository.ContentDAO; 047import org.ametys.cms.repository.ReactionableObject.ReactionType; 048import org.ametys.cms.repository.ReactionableObjectHelper; 049import org.ametys.cms.repository.ReportableObjectHelper; 050import org.ametys.cms.repository.comment.contributor.ContributorCommentableAmetysObject; 051import org.ametys.cms.rights.ContentRightAssignmentContext; 052import org.ametys.core.captcha.CaptchaHelper; 053import org.ametys.core.observation.Event; 054import org.ametys.core.observation.ObservationManager; 055import org.ametys.core.right.RightManager; 056import org.ametys.core.right.RightManager.RightResult; 057import org.ametys.core.ui.Callable; 058import org.ametys.core.user.CurrentUserProvider; 059import org.ametys.core.user.User; 060import org.ametys.core.user.UserIdentity; 061import org.ametys.core.user.UserManager; 062import org.ametys.core.user.directory.NotUniqueUserException; 063import org.ametys.core.user.population.PopulationContextHelper; 064import org.ametys.core.user.population.UserPopulation; 065import org.ametys.core.user.population.UserPopulationDAO; 066import org.ametys.core.util.DateUtils; 067import org.ametys.core.util.mail.SendMailHelper; 068import org.ametys.plugins.core.ui.user.ProfileImageResolverHelper; 069import org.ametys.plugins.core.user.UserHelper; 070import org.ametys.plugins.repository.AmetysObjectResolver; 071import org.ametys.plugins.repository.provider.RequestAttributeWorkspaceSelector; 072import org.ametys.runtime.authentication.AccessDeniedException; 073import org.ametys.runtime.config.Config; 074import org.ametys.runtime.i18n.I18nizableText; 075import org.ametys.runtime.plugin.component.AbstractLogEnabled; 076 077/** 078 * DAO for content's comments 079 * 080 */ 081public class CommentsDAO extends AbstractLogEnabled implements Component, Serviceable, Contextualizable 082{ 083 /** The Avalon role */ 084 public static final String ROLE = CommentsDAO.class.getName(); 085 086 /** The id of the right to moderate comment */ 087 public static final String COMMENT_MODERATE_RIGHT = "CMS_Rights_CommentModerate"; 088 /** The form input name for author name */ 089 public static final String FORM_AUTHOR_NAME = "name"; 090 /** The form input name for author email */ 091 public static final String FORM_AUTHOR_EMAIL = "email"; 092 /** The form input name for author email to hide */ 093 public static final String FORM_AUTHOR_HIDEEMAIL = "hide-email"; 094 /** The form input name for author url */ 095 public static final String FORM_AUTHOR_URL = "url"; 096 /** The form input name for content */ 097 public static final String FORM_CONTENTTEXT = "text"; 098 /** The form input name for captcha */ 099 public static final String FORM_CAPTCHA_KEY = "captcha-key"; 100 /** The form input name for captcha */ 101 public static final String FORM_CAPTCHA_VALUE = "captcha-value"; 102 103 /** The pattern to check url */ 104 public static final Pattern URL_VALIDATOR = Pattern.compile("^(https?:\\/\\/.+)?$"); 105 106 /** The content DAO */ 107 protected ContentDAO _contentDAO; 108 /** The Ametys object resolver */ 109 protected AmetysObjectResolver _resolver; 110 /** The observation manager */ 111 protected ObservationManager _observationManager; 112 /** The currenrt user provider */ 113 protected CurrentUserProvider _userProvider; 114 /** The user helper */ 115 protected UserHelper _userHelper; 116 /** The right manager */ 117 protected RightManager _rightManager; 118 /** Helper for reactionable object */ 119 protected ReactionableObjectHelper _reactionableHelper; 120 /** The user manager */ 121 protected UserManager _userManager; 122 /** The population context helper */ 123 protected PopulationContextHelper _populationContextHelper; 124 /** The user population DAO */ 125 protected UserPopulationDAO _userPopulationDAO; 126 127 /** The avalon context */ 128 protected Context _context; 129 130 public void service(ServiceManager smanager) throws ServiceException 131 { 132 _contentDAO = (ContentDAO) smanager.lookup(ContentDAO.ROLE); 133 _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE); 134 _userProvider = (CurrentUserProvider) smanager.lookup(CurrentUserProvider.ROLE); 135 _observationManager = (ObservationManager) smanager.lookup(ObservationManager.ROLE); 136 _userHelper = (UserHelper) smanager.lookup(UserHelper.ROLE); 137 _rightManager = (RightManager) smanager.lookup(RightManager.ROLE); 138 _reactionableHelper = (ReactionableObjectHelper) smanager.lookup(ReactionableObjectHelper.ROLE); 139 _populationContextHelper = (PopulationContextHelper) smanager.lookup(PopulationContextHelper.ROLE); 140 _userManager = (UserManager) smanager.lookup(UserManager.ROLE); 141 _userPopulationDAO = (UserPopulationDAO) smanager.lookup(UserPopulationDAO.ROLE); 142 } 143 144 public void contextualize(Context context) throws ContextException 145 { 146 _context = context; 147 } 148 149 /** 150 * Get the content's comments and user rights on comment features (commenting, reacting, reporting, deleting) 151 * @param contentId the content id 152 * @param contextualParameters the contextual parameters 153 * @return the comments and the user rights on comment features (commenting, reacting, reporting, deleting) 154 */ 155 @Callable (allowAnonymous = true, rights = Callable.READ_ACCESS, rightContext = ContentRightAssignmentContext.ID, paramIndex = 0, decorators = ForceDefaultRepositoryWorkspaceCallableDecorator.DECORATOR_ID) 156 public Map<String, Object> getComments(String contentId, Map<String, Object> contextualParameters) 157 { 158 Content content = _resolver.resolveById(contentId); 159 List<Map<String, Object>> comments2json = new ArrayList<>(); 160 161 if (content instanceof CommentableContent commentableContent) 162 { 163 List<Comment> comments = commentableContent.getComments(false, true); 164 for (Comment comment : comments) 165 { 166 comments2json.add(getComment(content, comment, 0, contextualParameters)); 167 } 168 } 169 170 return Map.of( 171 "rights", getCommentsUserRights(content), 172 "comments", comments2json 173 ); 174 } 175 176 /** 177 * Get the content's contributor comments 178 * @param contentId the content id 179 * @return the comments 180 */ 181 public List<Comment> getContributorComments(String contentId) 182 { 183 Content content = _resolver.resolveById(contentId); 184 185 List<Comment> contributorComments = new ArrayList<>(); 186 187 if (content instanceof ContributorCommentableAmetysObject commentableContent) 188 { 189 contributorComments.addAll(commentableContent.getContributorComments()); 190 } 191 192 return contributorComments; 193 } 194 195 /** 196 * Add a new comment 197 * @param contentId the content id 198 * @param commentId the id of of parent comment. Can be null if it is not a subcomment 199 * @param formValues the form's values 200 * @param contextualParameters the contextual parameters 201 * @return the results 202 */ 203 @Callable (allowAnonymous = true, rights = Callable.CHECKED_BY_IMPLEMENTATION, decorators = ForceDefaultRepositoryWorkspaceCallableDecorator.DECORATOR_ID) 204 public Map<String, Object> addComment(String contentId, String commentId, Map<String, Object> formValues, Map<String, Object> contextualParameters) 205 { 206 CommentableContent cContent = getContent(contentId); 207 if (!_contentDAO.canComment(cContent)) 208 { 209 throw new AccessDeniedException("User " + getCurrentUser() + " tried to add comment on content " + cContent.getId() + " without sufficient rights"); 210 } 211 212 List<Map<String, Object>> errors = getErrors(cContent, formValues); 213 214 if (errors.isEmpty()) 215 { 216 Map<String, Object> results = new HashMap<>(); 217 218 if (StringUtils.isNotBlank(commentId)) 219 { 220 Comment comment = cContent.getComment(commentId); 221 Comment subComment = comment.createSubComment(); 222 results = _setCommentAttributes(cContent, subComment, formValues); 223 results.put("comment", getComment(cContent, subComment, 1, contextualParameters)); 224 } 225 else 226 { 227 Comment comment = cContent.createComment(); 228 results = _setCommentAttributes(cContent, comment, formValues); 229 results.put("comment", getComment(cContent, comment, 0, contextualParameters)); 230 } 231 232 return results; 233 } 234 else 235 { 236 Map<String, Object> results = new HashMap<>(); 237 results.put("errors", errors); 238 return results; 239 } 240 } 241 242 /** 243 * Delete a comment 244 * @param contentId the content id 245 * @param commentId the comment id to remove 246 * @return the results 247 */ 248 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION, decorators = ForceDefaultRepositoryWorkspaceCallableDecorator.DECORATOR_ID) 249 public Map<String, Object> deleteComment(String contentId, String commentId) 250 { 251 CommentableContent content = getContent(contentId); 252 Comment comment = content.getComment(commentId); 253 254 if (!canDeleteComment(content, comment)) 255 { 256 throw new AccessDeniedException("User " + getCurrentUser() + " tried to delete comment on content " + content + " without sufficient rights"); 257 } 258 259 Map<String, Object> results = new HashMap<>(); 260 261 Map<String, Object> eventParams = new HashMap<>(); 262 eventParams.put(ObservationConstants.ARGS_CONTENT, content); 263 eventParams.put(ObservationConstants.ARGS_COMMENT_ID, comment.getId()); 264 eventParams.put(ObservationConstants.ARGS_COMMENT_AUTHOR, comment.getAuthorName()); 265 eventParams.put(ObservationConstants.ARGS_COMMENT_AUTHOR_EMAIL, comment.getAuthorEmail()); 266 eventParams.put(ObservationConstants.ARGS_COMMENT_VALIDATED, comment.isValidated()); 267 eventParams.put(ObservationConstants.ARGS_COMMENT_CREATION_DATE, comment.getCreationDate()); 268 269 eventParams.put("comment.content", comment.getContent()); 270 271 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_COMMENT_DELETING, getCurrentUser(), eventParams)); 272 273 comment.remove(); 274 content.saveChanges(); 275 276 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_COMMENT_DELETED, getCurrentUser(), eventParams)); 277 278 results.put("deleted", true); 279 results.put("contentId", content.getId()); 280 results.put("commentId", comment.getId()); 281 282 return results; 283 } 284 285 /** 286 * Determines if current user is allowed to delete any comment 287 * @param contentId the content id 288 * @return true if the current user is allowed 289 */ 290 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION, decorators = ForceDefaultRepositoryWorkspaceCallableDecorator.DECORATOR_ID) 291 public boolean canDeleteComment(String contentId) 292 { 293 CommentableContent content = getContent(contentId); 294 return canDeleteComment(content); 295 } 296 297 /** 298 * Determines if current user is allowed to delete any comment 299 * @param content the content 300 * @return true if the current user is allowed 301 */ 302 public boolean canDeleteComment(Content content) 303 { 304 UserIdentity currentUser = getCurrentUser(); 305 return currentUser != null && _rightManager.hasRight(currentUser, COMMENT_MODERATE_RIGHT, content).equals(RightResult.RIGHT_ALLOW); 306 } 307 308 /** 309 * Determines if current user is allowed to delete a comment 310 * @param content the content 311 * @param comment the comment. Cannot be null. 312 * @return true if the current user is allowed 313 */ 314 public boolean canDeleteComment(Content content, Comment comment) 315 { 316 if (canDeleteComment(content)) 317 { 318 return true; 319 } 320 321 UserIdentity currentUser = getCurrentUser(); 322 323 // if the user don't have the right to comment a content then it can't manage his own comment anymore 324 if (currentUser != null && _contentDAO.canComment(content)) 325 { 326 User user = _userManager.getUser(currentUser); 327 String authorEmail = comment.getAuthorEmail(); 328 329 // Check if the current user is the author of the comment 330 return user != null && Strings.CS.equals(authorEmail, user.getEmail()); 331 } 332 333 return false; 334 } 335 336 /** 337 * React (like or unlike) to a comment 338 * @param contentId the content id 339 * @param commentId the comment id 340 * @return the results 341 */ 342 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION, decorators = ForceDefaultRepositoryWorkspaceCallableDecorator.DECORATOR_ID) 343 public Map<String, Object> likeOrUnlikeComment(String contentId, String commentId) 344 { 345 return likeOrUnlikeComment(contentId, commentId, null); 346 } 347 348 /** 349 * React (like or unlike) to a comment 350 * @param contentId the content id 351 * @param commentId the comment id 352 * @param remove true to remove the reaction, false to add reaction. If null, check if the current user has already like the comment. 353 * @return the results 354 */ 355 @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION, decorators = ForceDefaultRepositoryWorkspaceCallableDecorator.DECORATOR_ID) 356 public Map<String, Object> likeOrUnlikeComment(String contentId, String commentId, Boolean remove) 357 { 358 Map<String, Object> results = new HashMap<>(); 359 360 CommentableContent content = getContent(contentId); 361 362 UserIdentity currentUser = getCurrentUser(); 363 if (currentUser == null) 364 { 365 throw new AccessDeniedException("Anonymous user is not allowed to react to a comment"); 366 } 367 else if (!_contentDAO.canReact(content)) 368 { 369 throw new AccessDeniedException("User " + currentUser + " tried to react to a comment on content " + content + " without sufficient rights"); 370 } 371 372 Comment comment = content.getComment(commentId); 373 374 List<UserIdentity> likers = comment.getReactionUsers(ReactionType.LIKE); 375 int nbLikes = likers.size(); 376 377 if (Boolean.TRUE.equals(remove) 378 || remove == null && likers.contains(currentUser)) 379 { 380 comment.removeReaction(currentUser, ReactionType.LIKE); 381 results.put("liked", false); 382 nbLikes--; 383 } 384 else 385 { 386 comment.addReaction(currentUser, ReactionType.LIKE); 387 results.put("liked", true); 388 nbLikes++; 389 } 390 391 results.put("nbLikes", nbLikes); 392 393 content.saveChanges(); 394 395 Map<String, Object> eventParams = new HashMap<>(); 396 eventParams.put(ObservationConstants.ARGS_CONTENT, content); 397 eventParams.put(ObservationConstants.ARGS_COMMENT, comment); 398 eventParams.put(ObservationConstants.ARGS_REACTION_TYPE, ReactionType.LIKE); 399 eventParams.put(ObservationConstants.ARGS_REACTION_ISSUER, currentUser); 400 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_COMMENT_REACTION_CHANGED, getCurrentUser(), eventParams)); 401 402 results.put("contentId", content.getId()); 403 results.put("commentId", comment.getId()); 404 return results; 405 } 406 407 /** 408 * Get the commentable content 409 * @param contentId The content id 410 * @return The content 411 */ 412 protected CommentableContent getContent (String contentId) 413 { 414 Request request = ContextHelper.getRequest(_context); 415 String currentWorkspace = RequestAttributeWorkspaceSelector.getForcedWorkspace(request); 416 417 try 418 { 419 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, "default"); 420 return _resolver.resolveById(contentId); 421 } 422 finally 423 { 424 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWorkspace); 425 } 426 } 427 428 /** 429 * Set comment attributes 430 * @param content the content 431 * @param comment the comment 432 * @param formValues the form's values 433 * @return the result 434 */ 435 protected Map<String, Object> _setCommentAttributes(CommentableContent content, Comment comment, Map<String, Object> formValues) 436 { 437 String authorName = (String) formValues.get(FORM_AUTHOR_NAME); 438 String authorEmail = (String) formValues.get(FORM_AUTHOR_EMAIL); 439 String authorUrl = (String) formValues.get(FORM_AUTHOR_URL); 440 String text = (String) formValues.get(FORM_CONTENTTEXT); 441 boolean hideEmail = formValues.containsKey(FORM_AUTHOR_HIDEEMAIL) && Boolean.TRUE.equals(formValues.get(FORM_AUTHOR_HIDEEMAIL)); 442 443 comment.setAuthorName(authorName); 444 comment.setAuthorEmail(authorEmail); 445 comment.setEmailHiddenStatus(hideEmail); 446 comment.setAuthorURL(authorUrl); 447 comment.setContent(text.replaceAll("\r", "")); 448 449 boolean isValidated = isValidatedByDefault(content); 450 comment.setValidated(isValidated); 451 452 content.saveChanges(); 453 454 if (isValidated) 455 { 456 Map<String, Object> eventParams = new HashMap<>(); 457 eventParams.put(ObservationConstants.ARGS_CONTENT, content); 458 eventParams.put(ObservationConstants.ARGS_COMMENT, comment); 459 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_COMMENT_VALIDATED, getCurrentUser(), eventParams)); 460 } 461 else 462 { 463 Map<String, Object> eventParams = new HashMap<>(); 464 eventParams.put(ObservationConstants.ARGS_CONTENT, content); 465 eventParams.put(ObservationConstants.ARGS_COMMENT, comment); 466 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_COMMENT_ADDED, getCurrentUser(), eventParams)); 467 } 468 469 Map<String, Object> results = new HashMap<>(); 470 471 results.put("published", comment.isValidated()); 472 results.put("contentId", content.getId()); 473 results.put("commentId", comment.getId()); 474 475 return results; 476 } 477 478 /** 479 * Report a comment 480 * @param contentId the content id 481 * @param commentId the comment id 482 * @return the results 483 */ 484 @Callable (allowAnonymous = true, rights = Callable.CHECKED_BY_IMPLEMENTATION, decorators = ForceDefaultRepositoryWorkspaceCallableDecorator.DECORATOR_ID) 485 public Map<String, Object> reportComment(String contentId, String commentId) 486 { 487 CommentableContent content = getContent(contentId); 488 if (!_contentDAO.canReport(content)) 489 { 490 throw new AccessDeniedException("Unable to report the comment '" + commentId + "' of content '" + contentId + "'. Current user don't have sufficient right."); 491 } 492 Comment comment = content.getComment(commentId); 493 494 comment.addReport(); 495 content.saveChanges(); 496 497 Map<String, Object> eventParams = new HashMap<>(); 498 eventParams.put(ObservationConstants.ARGS_CONTENT, content); 499 eventParams.put(ObservationConstants.ARGS_COMMENT, comment); 500 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_COMMENT_REPORTED, getCurrentUser(), eventParams)); 501 502 Map<String, Object> results = new HashMap<>(); 503 results.put("reported", true); 504 results.put("contentId", content.getId()); 505 results.put("commentId", comment.getId()); 506 return results; 507 } 508 509 /** 510 * Get the validation flag default value for a content asking all listeners 511 * @param content The content having a new comment 512 * @return a positive value if the comments have to be validated by default or a negative value in the other case. The absolute value is the priority of your listener. E.G. If a listener set +1 and another -10: the sum is negative (so comments not validated be default). 513 */ 514 public boolean isValidatedByDefault(Content content) 515 { 516 boolean postValidation = Config.getInstance().getValue("cms.contents.comments.postvalidation"); 517 return postValidation; 518 } 519 520 /** 521 * Checks if a captcha have to be checked. 522 * @param content The content to comment 523 * @param formValues The form values 524 * @return true if the comments have to be protected by a captcha or false otherwise 525 */ 526 public boolean isCaptchaRequired(Content content, Map<String, Object> formValues) 527 { 528 return false; 529 } 530 531 /** 532 * Get errors when submitting comment 533 * @param content The content to comment 534 * @param formValues The form values to submit a comment 535 * @return An list of error messages (empty if no errors) 536 */ 537 public List<Map<String, Object>> getErrors(CommentableContent content, Map<String, Object> formValues) 538 { 539 List<Map<String, Object>> errors = new ArrayList<>(); 540 541 String name = (String) formValues.get(FORM_AUTHOR_NAME); 542 if (StringUtils.isBlank(name)) 543 { 544 Map<String, Object> error = new HashMap<>(); 545 error.put("name", FORM_AUTHOR_NAME); 546 error.put("error", new I18nizableText("plugin.cms", "PLUGINS_CMS_CONTENT_COMMENTS_ADD_ERROR_NAME")); 547 errors.add(error); 548 } 549 550 String email = (String) formValues.get(FORM_AUTHOR_EMAIL); 551 if (!SendMailHelper.EMAIL_VALIDATION.matcher(StringUtils.trimToEmpty(email)).matches()) 552 { 553 Map<String, Object> error = new HashMap<>(); 554 error.put("name", FORM_AUTHOR_EMAIL); 555 error.put("error", new I18nizableText("plugin.cms", "PLUGINS_CMS_CONTENT_COMMENTS_ADD_ERROR_EMAIL")); 556 errors.add(error); 557 } 558 559 String url = (String) formValues.get(FORM_AUTHOR_URL); 560 if (!URL_VALIDATOR.matcher(StringUtils.trimToEmpty(url)).matches()) 561 { 562 Map<String, Object> error = new HashMap<>(); 563 error.put("name", FORM_AUTHOR_URL); 564 error.put("error", new I18nizableText("plugin.cms", "PLUGINS_CMS_CONTENT_COMMENTS_ADD_ERROR_URL")); 565 errors.add(error); 566 } 567 568 String text = (String) formValues.get(FORM_CONTENTTEXT); 569 if (StringUtils.isBlank(text)) 570 { 571 Map<String, Object> error = new HashMap<>(); 572 error.put("name", FORM_CONTENTTEXT); 573 error.put("error", new I18nizableText("plugin.cms", "PLUGINS_CMS_CONTENT_COMMENTS_ADD_ERROR_CONTENT")); 574 errors.add(error); 575 } 576 577 if (isCaptchaRequired(content, formValues)) 578 { 579 String captchaKey = (String) formValues.get(FORM_CAPTCHA_KEY); 580 String captchaValue = (String) formValues.get(FORM_CAPTCHA_VALUE); 581 if (!CaptchaHelper.checkAndInvalidate(captchaKey, captchaValue)) 582 { 583 Map<String, Object> error = new HashMap<>(); 584 error.put("name", FORM_CAPTCHA_VALUE); 585 error.put("error", new I18nizableText("plugin.cms", "PLUGINS_CMS_CONTENT_COMMENTS_ADD_ERROR_CAPTCHA")); 586 errors.add(error); 587 } 588 } 589 590 return errors; 591 } 592 /** 593 * Get the current user 594 * @return The current user 595 */ 596 protected UserIdentity getCurrentUser() 597 { 598 return _userProvider.getUser(); 599 } 600 601 /** 602 * Get JSON representation of a comment 603 * @param content The content 604 * @param comment the comment 605 * @param level the level of comment (0 for parent comment, 1 for sub-comment, etc ....) 606 * @param contextualParameters the contextual parameters 607 * @return the comment as JSON 608 */ 609 public Map<String, Object> getComment(Content content, Comment comment, int level, Map<String, Object> contextualParameters) 610 { 611 Map<String, Object> comment2json = comment2JSON(comment, true, contextualParameters); 612 613 comment2json.put("content-id", content.getId()); 614 comment2json.put("level", level); 615 616 List<Comment> subComments = comment.getSubComment(false, true); 617 if (!subComments.isEmpty()) 618 { 619 List<Map<String, Object>> subComments2json = new ArrayList<>(); 620 for (Comment subComment : subComments) 621 { 622 subComments2json.add(getComment(content, subComment, level + 1, contextualParameters)); 623 } 624 comment2json.put("sub-comments", subComments2json); 625 } 626 627 comment2json.put("canDelete", canDeleteComment(content, comment)); 628 comment2json.put("canReact", _contentDAO.canReact(content)); 629 comment2json.put("canComment", _contentDAO.canComment(content)); 630 comment2json.put("canReport", _contentDAO.canReport(content)); 631 632 return comment2json; 633 } 634 635 /** 636 * Comment to JSON (ignore sub comments) 637 * @param comment the comment 638 * @param withMentions true to have mentions 639 * @param contextualParameters the contextual parameters 640 * @return comment as JSON 641 */ 642 public Map<String, Object> comment2JSON(Comment comment, boolean withMentions, Map<String, Object> contextualParameters) 643 { 644 Map<String, Object> comment2json = new HashMap<>(); 645 646 comment2json.put("id", comment.getId()); 647 comment2json.put("creation-date", comment.getCreationDate()); 648 comment2json.put("modification-date", comment.getLastModificationDate()); 649 650 comment2json.putAll(getCommentAuthor(comment, contextualParameters)); 651 652 if (comment.getContent() != null) 653 { 654 String commentContent = comment.getContent(); 655 comment2json.put("content", commentContent); 656 657 if (withMentions) 658 { 659 List<Map<String, Object>> mentionedUsers2json = comment.extractMentions() 660 .stream() 661 .filter(Objects::nonNull) 662 .map(userIdentity -> getUserPropertiesFromIdentity(userIdentity, contextualParameters)) 663 .toList(); 664 if (!mentionedUsers2json.isEmpty()) 665 { 666 comment2json.put("mentions", mentionedUsers2json); 667 } 668 } 669 } 670 671 Comment referencedComment = comment.getReferencedComment(); 672 if (referencedComment != null) 673 { 674 comment2json.put("parent-id", referencedComment.getId()); 675 comment2json.put("parent-author", getUserPropertiesFromIdentity(referencedComment.getAuthor(), contextualParameters)); 676 comment2json.put("parent-content", referencedComment.getContent()); 677 comment2json.put("parent-creation-date", referencedComment.getCreationDate()); 678 } 679 680 List<Map<String, Object>> reactions2json = _reactionableHelper.reactionsToJson(comment); 681 comment2json.put("reactions", reactions2json); 682 683 comment2json.put("reports", comment.getReportsCount()); 684 return comment2json; 685 } 686 687 /** 688 * Get the current user or anonymous user rights on comment features (commenting, reacting, reporting, deleting) 689 * @param contentId The content's id 690 * @return the rights as a map with "canComment", "canReact", "canReport", "canDeleteAny" and "canDeleteOwn" keys 691 */ 692 @Callable (allowAnonymous = true, rights = Callable.NO_CHECK_REQUIRED) 693 public Map<String, Boolean> getCommentsUserRights(String contentId) 694 { 695 Content content = _resolver.resolveById(contentId); 696 return getCommentsUserRights(content); 697 } 698 699 /** 700 * Get the current user or anonymous user rights on comment features (commenting, reacting, reporting, deleting) 701 * @param content The content 702 * @return the rights as a map with "canComment", "canReact", "canReport", "canDeleteAny" and "canDeleteOwn" keys 703 */ 704 public Map<String, Boolean> getCommentsUserRights(Content content) 705 { 706 return Map.of( 707 "canComment", _contentDAO.canComment(content), 708 "canReact", _contentDAO.canReact(content), 709 "canReport", _contentDAO.canReport(content), 710 "canDeleteAny", canDeleteComment(content), 711 "canDeleteOwn", getCurrentUser() != null && _contentDAO.canComment(content) // user can delete his own comment if he can comment to the content 712 ); 713 } 714 715 /** 716 * Get JSON representation of a comment's author 717 * @param comment the comment 718 * @param contextualParameters the contextual parameters 719 * @return the comment's author as JSON 720 */ 721 protected Map<String, Object> getCommentAuthor(Comment comment, Map<String, Object> contextualParameters) 722 { 723 Map<String, Object> author2json = new HashMap<>(); 724 725 User authorUser = null; 726 UserIdentity authorIdentity = comment.getAuthor(); 727 if (authorIdentity != null) 728 { 729 authorUser = _userManager.getUser(authorIdentity); 730 Map<String, Object> user2json = Optional.ofNullable(authorUser) 731 .map(author -> getUserProperties(author, contextualParameters)) 732 .orElseGet(() -> getUserIdentityProperties(authorIdentity, contextualParameters)); 733 author2json.put("author", user2json); 734 } 735 else 736 { 737 String authorEmail = comment.getAuthorEmail(); 738 if (StringUtils.isNotBlank(authorEmail)) 739 { 740 getUserByEmail(authorEmail, contextualParameters) 741 .map(author -> getUserProperties(author, contextualParameters)) 742 .ifPresent(json -> author2json.put("author", json)); 743 } 744 } 745 746 author2json.put("author-name", Optional.ofNullable(comment.getAuthorName()).orElse(Optional.ofNullable(authorUser).map(u -> u.getFullName()).orElse(StringUtils.EMPTY))); 747 748 if (!comment.isEmailHidden()) 749 { 750 author2json.put("author-email", Optional.ofNullable(comment.getAuthorEmail()).orElse(Optional.ofNullable(authorUser).map(u -> u.getEmail()).orElse(StringUtils.EMPTY))); 751 } 752 753 String authorURL = comment.getAuthorURL(); 754 if (StringUtils.isNotBlank(authorURL)) 755 { 756 author2json.put("author-url", authorURL); 757 } 758 759 return author2json; 760 } 761 762 /** 763 * Get the author as a User if exists 764 * @param userEmail the email of the user to retrieve 765 * @param contextualParameters the contextual parameters 766 * @return the user or null 767 */ 768 protected Optional<User> getUserByEmail(String userEmail, Map<String, Object> contextualParameters) 769 { 770 Request request = ContextHelper.getRequest(_context); 771 772 Set<String> userPopulationsOnSite = _populationContextHelper.getUserPopulationsOnContexts(getUserPopulationsContexts(request, contextualParameters), false, false); 773 774 try 775 { 776 return Optional.ofNullable(_userManager.getUserByEmail(userPopulationsOnSite, userEmail)); 777 } 778 catch (NotUniqueUserException e) 779 { 780 getLogger().error("Cannot find user because 2 or more users match", e); 781 return Optional.empty(); 782 } 783 } 784 785 /** 786 * Get JSON representation of a user from its identity. If the user does not exist, get all known identity properties 787 * @param userIdentity the user identity 788 * @param contextualParameters the contextual parameters 789 * @return the user as JSON 790 */ 791 public Map<String, Object> getUserPropertiesFromIdentity(UserIdentity userIdentity, Map<String, Object> contextualParameters) 792 { 793 return Optional.ofNullable(_userManager.getUser(userIdentity)) 794 .map(user -> getUserProperties(user, contextualParameters)) 795 .orElseGet(() -> getUserIdentityProperties(userIdentity, contextualParameters)); 796 } 797 798 /** 799 * Get JSON representation of a user 800 * @param user the user 801 * @param contextualParameters the contextual parameters 802 * @return the user as JSON 803 */ 804 protected Map<String, Object> getUserProperties(User user, Map<String, Object> contextualParameters) 805 { 806 Map<String, Object> user2json = _userHelper.user2json(user, true); 807 user2json.put("imgUrl", ProfileImageResolverHelper.resolve(user.getIdentity().getLogin(), user.getIdentity().getPopulationId(), 64, null)); 808 return user2json; 809 } 810 811 /** 812 * Get JSON representation of a user identity. Put all known data in the json result 813 * @param userIdentity the user identity 814 * @param contextualParameters the contextual parameters 815 * @return the user identity as JSON 816 */ 817 protected Map<String, Object> getUserIdentityProperties(UserIdentity userIdentity, Map<String, Object> contextualParameters) 818 { 819 Map<String, Object> user2json = new HashMap<>(); 820 821 user2json.put("login", userIdentity.getLogin()); 822 user2json.put("populationId", userIdentity.getPopulationId()); 823 824 UserPopulation userPopulation = _userPopulationDAO.getUserPopulation(userIdentity.getPopulationId()); 825 if (userPopulation != null) 826 { 827 user2json.put("populationLabel", userPopulation.getLabel()); 828 } 829 830 return user2json; 831 } 832 833 /** 834 * Get the user population contexts 835 * @param request The request 836 * @param contextualParameters The contextual parameters 837 * @return The contexts 838 */ 839 protected List<String> getUserPopulationsContexts(Request request, Map<String, Object> contextualParameters) 840 { 841 return List.of("/application"); 842 } 843 844 /** 845 * SAX a comment 846 * @param contentHandler the content handler 847 * @param comment the comment 848 * @param level the level of comment 849 * @param contextualParameters the contextual parameters 850 * @throws SAXException if an error occurred hile saxing 851 */ 852 public void saxComment(ContentHandler contentHandler, Comment comment, int level, Map<String, Object> contextualParameters) throws SAXException 853 { 854 AttributesImpl attrs = new AttributesImpl(); 855 856 attrs.addCDATAAttribute("id", comment.getId()); 857 attrs.addCDATAAttribute("creation-date", DateUtils.zonedDateTimeToString(comment.getCreationDate())); 858 attrs.addCDATAAttribute("level", String.valueOf(level)); 859 attrs.addCDATAAttribute("is-validated", String.valueOf(comment.isValidated())); 860 attrs.addCDATAAttribute("is-email-hidden", String.valueOf(comment.isEmailHidden())); 861 862 UserIdentity authorIdentity = comment.getAuthor(); 863 if (authorIdentity == null) 864 { 865 String authorName = comment.getAuthorName(); 866 if (StringUtils.isNotBlank(authorName)) 867 { 868 attrs.addCDATAAttribute("author-name", authorName); 869 } 870 871 String authorEmail = comment.getAuthorEmail(); 872 if (!comment.isEmailHidden() && StringUtils.isNotBlank(authorEmail)) 873 { 874 attrs.addCDATAAttribute("author-email", authorEmail); 875 } 876 877 String authorURL = comment.getAuthorURL(); 878 if (!StringUtils.isBlank(authorURL)) 879 { 880 attrs.addCDATAAttribute("author-url", authorURL); 881 } 882 } 883 884 XMLUtils.startElement(contentHandler, "comment", attrs); 885 886 if (authorIdentity != null) 887 { 888 User author = _userManager.getUser(authorIdentity); 889 if (author != null) 890 { 891 _userHelper.saxUser(author, contentHandler, "author"); 892 } 893 else 894 { 895 // If author does not exist anymore, still generate sax event for its identity 896 saxUserIdentity(contentHandler, authorIdentity, "author"); 897 } 898 } 899 else 900 { 901 String authorEmail = comment.getAuthorEmail(); 902 if (StringUtils.isNotBlank(authorEmail)) 903 { 904 Optional<User> author = getUserByEmail(authorEmail, Map.of()); 905 if (author.isPresent()) 906 { 907 _userHelper.saxUser(author.get(), contentHandler, "author"); 908 } 909 } 910 } 911 912 if (comment.getContent() != null) 913 { 914 String[] contents = comment.getContent().split("\r?\n"); 915 for (String c : contents) 916 { 917 XMLUtils.createElement(contentHandler, "p", c); 918 } 919 } 920 921 // The generated SAXed events for the comments' reaction have changed. 922 // In the SAXed events of the ContentGenerator (that should one day use this ContentSaxer): 923 // - there is a "nb-like" attributes on the comment node that disappears here 924 // - reactions are SAXed as a simple list of ("likers") 925 _reactionableHelper.saxReactions(comment, contentHandler); 926 927 ReportableObjectHelper.saxReports(comment, contentHandler); 928 929 // Additional properties 930 saxCommentAdditionalProperties(contentHandler, comment, level, contextualParameters); 931 932 List<Comment> subComments = comment.getSubComment(false, true); 933 if (!subComments.isEmpty()) 934 { 935 XMLUtils.startElement(contentHandler, "sub-comments"); 936 937 for (Comment subComment : subComments) 938 { 939 saxComment(contentHandler, subComment, level + 1, contextualParameters); 940 } 941 942 XMLUtils.endElement(contentHandler, "sub-comments"); 943 } 944 945 XMLUtils.endElement(contentHandler, "comment"); 946 } 947 948 /** 949 * Generate SAX events for all known data of the given user identity 950 * @param contentHandler the content handler 951 * @param userIdentity the user identity 952 * @param tagName The XML tag for saxed user 953 * @throws SAXException if an error occurred while generating SAX events 954 */ 955 protected void saxUserIdentity(ContentHandler contentHandler, UserIdentity userIdentity, String tagName) throws SAXException 956 { 957 AttributesImpl attr = new AttributesImpl(); 958 attr.addCDATAAttribute("login", userIdentity.getLogin()); 959 attr.addCDATAAttribute("population", userIdentity.getPopulationId()); 960 961 XMLUtils.startElement(contentHandler, tagName, attr); 962 963 UserPopulation userPopulation = _userPopulationDAO.getUserPopulation(userIdentity.getPopulationId()); 964 if (userPopulation != null) 965 { 966 userPopulation.getLabel().toSAX(contentHandler, "populationLabel"); 967 } 968 969 XMLUtils.endElement(contentHandler, tagName); 970 } 971 972 /** 973 * SAX additional comment properties 974 * @param contentHandler the content handler 975 * @param comment the comment 976 * @param level the level of comment 977 * @param contextualParameters the contextual parameters 978 * @throws SAXException if an error occurred while saxing 979 */ 980 protected void saxCommentAdditionalProperties(ContentHandler contentHandler, Comment comment, int level, Map<String, Object> contextualParameters) throws SAXException 981 { 982 // Nothing 983 } 984}