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