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